Grid
Responsive grid layout with container query support.
import { Grid, Frame } from "ui-lab-components";
export function Example() {
return (
<Grid columns={3} gap="md">
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "9rem", gridRow: "span 2" }} />
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "5rem", gridColumn: "span 2" }} />
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "3rem" }} />
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "3rem" }} />
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "6rem", gridColumn: "span 2" }} />
<Frame pathStroke="dashed" style={{ width: "100%", minHeight: "4rem" }} />
</Grid>
);
}Track Placement
Adjust columns, gap, alignment, and auto-placement flow to explore the CSS Grid track model.
1024px
"use client";
import React from 'react'
import { Grid, Frame } from 'ui-lab-components'
import type { GridProps } from 'ui-lab-components'
import type { ControlDef } from '@/types'
type GridColumnsValue = '1' | '2' | '3' | '4' | '5' | '6' | 'auto-fit' | 'auto-fill';
type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const BASE_CELL_STYLE = {
'--frame-fill': 'var(--background-900)',
'--frame-stroke-color': 'var(--background-600)',
} as React.CSSProperties;
export const controls: ControlDef[] = [
{
name: 'columns',
label: 'Columns',
type: 'select',
options: [
{ label: '1 Column', value: '1' },
{ label: '2 Columns', value: '2' },
{ label: '3 Columns', value: '3' },
{ label: '4 Columns', value: '4' },
{ label: '5 Columns', value: '5' },
{ label: '6 Columns', value: '6' },
{ label: 'Auto Fit', value: 'auto-fit' },
{ label: 'Auto Fill', value: 'auto-fill' },
],
defaultValue: '3',
},
{
name: 'gap',
label: 'Gap Token',
type: 'select',
options: [
{ label: 'Extra Small', value: 'xs' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
],
defaultValue: 'md',
},
{
name: 'justifyItems',
label: 'Inline Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
],
defaultValue: 'stretch',
},
{
name: 'alignItems',
label: 'Block Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
{ label: 'Baseline', value: 'baseline' },
],
defaultValue: 'stretch',
},
{
name: 'autoFlow',
label: 'Auto Placement',
type: 'select',
options: [
{ label: 'Row', value: 'row' },
{ label: 'Column', value: 'column' },
{ label: 'Row Dense', value: 'row-dense' },
{ label: 'Column Dense', value: 'column-dense' },
],
defaultValue: 'row',
},
{
name: 'frameCount',
label: 'Panels',
type: 'stepper',
defaultValue: 6,
min: 4,
max: 12,
step: 1,
},
];
export const previewLayout = 'start' as const;
export const resizable = true;
function getColumns(value: unknown): GridColumnsValue {
if (value === '1' || value === '2' || value === '4' || value === '5' || value === '6' || value === 'auto-fit' || value === 'auto-fill') return value;
return '3';
}
function toColumns(value: GridColumnsValue): number | 'auto-fit' | 'auto-fill' {
if (value === 'auto-fit' || value === 'auto-fill') return value;
return Number(value);
}
function getApproxColumnCount(value: GridColumnsValue) {
if (value === 'auto-fit' || value === 'auto-fill') return 4;
return Number(value);
}
function getGap(value: unknown): GridGap {
if (value === 'xs' || value === 'sm' || value === 'lg' || value === 'xl') return value;
return 'md';
}
function getJustifyItems(value: unknown): NonNullable<GridProps['justifyItems']> {
if (value === 'start' || value === 'center' || value === 'end') return value;
return 'stretch';
}
function getAlignItems(value: unknown): NonNullable<GridProps['alignItems']> {
if (value === 'start' || value === 'center' || value === 'end' || value === 'baseline') return value;
return 'stretch';
}
function getAutoFlow(value: unknown): NonNullable<GridProps['autoFlow']> {
if (value === 'column' || value === 'row-dense' || value === 'column-dense') return value;
return 'row';
}
function getFrameCount(value: unknown, min: number, max: number, fallback: number) {
const n = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(n)) return fallback;
return Math.min(max, Math.max(min, Math.round(n)));
}
function span(trackCount: number, desired: number) {
return `span ${Math.max(1, Math.min(desired, trackCount))}`;
}
function getSpecs(trackCount: number, frameCount: number) {
if (trackCount <= 2) {
const pattern = [{ minHeight: '5rem' }, { minHeight: '3rem' }, { minHeight: '4.5rem' }];
return [
{ minHeight: '3rem' },
{ minHeight: '8rem' },
...Array.from({ length: Math.max(frameCount - 2, 0) }, (_, i) => ({ minHeight: pattern[i % pattern.length].minHeight })),
].map(s => ({ style: { width: '100%', ...s } as React.CSSProperties }));
}
if (trackCount === 3) {
const pattern = [
{ width: '100%', minHeight: '6rem', gridColumn: 'span 2' },
{ width: '100%', minHeight: '3rem' },
{ width: '100%', minHeight: '4.5rem' },
];
return [
{ style: { width: '100%', minHeight: '9rem', gridColumn: 'span 1', gridRow: 'span 2' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '5rem', gridColumn: 'span 2' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as React.CSSProperties },
...Array.from({ length: Math.max(frameCount - 4, 0) }, (_, i) => ({ style: pattern[i % pattern.length] as React.CSSProperties })),
];
}
const pattern = [
{ width: '100%', minHeight: '6.5rem', gridColumn: span(trackCount, 2) },
{ width: '100%', minHeight: '3rem' },
{ width: '100%', minHeight: '4rem' },
{ width: '100%', minHeight: '5rem', gridColumn: span(trackCount, 2) },
];
return [
{ style: { width: '100%', minHeight: '10rem', gridRow: 'span 2' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '6rem', gridColumn: span(trackCount - 1, trackCount - 1) } as React.CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as React.CSSProperties },
...Array.from({ length: Math.max(frameCount - 4, 0) }, (_, i) => ({ style: pattern[i % pattern.length] as React.CSSProperties })),
];
}
export function renderPreview(props: Record<string, unknown>) {
const columns = getColumns(props.columns);
const frameCount = getFrameCount(props.frameCount, 4, 12, 6);
const specs = getSpecs(getApproxColumnCount(columns), frameCount);
return (
<Grid
columns={toColumns(columns)}
gap={getGap(props.gap)}
justifyItems={getJustifyItems(props.justifyItems)}
alignItems={getAlignItems(props.alignItems)}
autoFlow={getAutoFlow(props.autoFlow)}
className="w-full"
>
{specs.map((spec, index) => (
<Frame key={index} pathStroke="dashed" style={{ ...BASE_CELL_STYLE, ...spec.style }} className="w-full h-full">
<div className="size-full" />
</Frame>
))}
</Grid>
);
}
export default function Example() {
return renderPreview({ columns: '3', gap: 'md', justifyItems: 'stretch', alignItems: 'stretch', autoFlow: 'row', frameCount: 6 });
}Editorial Spans
Dense auto-flow grid with mixed column spans for editorial content layouts.
1024px
"use client";
import React from 'react'
import { Grid, Frame } from 'ui-lab-components'
import type { GridProps } from 'ui-lab-components'
import type { ControlDef } from '@/types'
type GridColumnsValue = '1' | '2' | '3' | '4' | '5' | '6' | 'auto-fit' | 'auto-fill';
type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const BASE_CELL_STYLE = {
'--frame-fill': 'var(--background-900)',
'--frame-stroke-color': 'var(--background-600)',
} as React.CSSProperties;
export const controls: ControlDef[] = [
{
name: 'columns',
label: 'Columns',
type: 'select',
options: [
{ label: '1 Column', value: '1' },
{ label: '2 Columns', value: '2' },
{ label: '3 Columns', value: '3' },
{ label: '4 Columns', value: '4' },
{ label: '5 Columns', value: '5' },
{ label: '6 Columns', value: '6' },
{ label: 'Auto Fit', value: 'auto-fit' },
{ label: 'Auto Fill', value: 'auto-fill' },
],
defaultValue: '4',
},
{
name: 'gap',
label: 'Gap Token',
type: 'select',
options: [
{ label: 'Extra Small', value: 'xs' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
],
defaultValue: 'md',
},
{
name: 'justifyItems',
label: 'Inline Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
],
defaultValue: 'stretch',
},
{
name: 'alignItems',
label: 'Block Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
{ label: 'Baseline', value: 'baseline' },
],
defaultValue: 'stretch',
},
{
name: 'autoFlow',
label: 'Auto Placement',
type: 'select',
options: [
{ label: 'Row', value: 'row' },
{ label: 'Column', value: 'column' },
{ label: 'Row Dense', value: 'row-dense' },
{ label: 'Column Dense', value: 'column-dense' },
],
defaultValue: 'row-dense',
},
{
name: 'frameCount',
label: 'Panels',
type: 'stepper',
defaultValue: 7,
min: 5,
max: 12,
step: 1,
},
];
export const previewLayout = 'start' as const;
export const resizable = true;
function getColumns(value: unknown): GridColumnsValue {
if (value === '1' || value === '2' || value === '4' || value === '5' || value === '6' || value === 'auto-fit' || value === 'auto-fill') return value;
return '3';
}
function toColumns(value: GridColumnsValue): number | 'auto-fit' | 'auto-fill' {
if (value === 'auto-fit' || value === 'auto-fill') return value;
return Number(value);
}
function getApproxColumnCount(value: GridColumnsValue) {
if (value === 'auto-fit' || value === 'auto-fill') return 4;
return Number(value);
}
function getGap(value: unknown): GridGap {
if (value === 'xs' || value === 'sm' || value === 'lg' || value === 'xl') return value;
return 'md';
}
function getJustifyItems(value: unknown): NonNullable<GridProps['justifyItems']> {
if (value === 'start' || value === 'center' || value === 'end') return value;
return 'stretch';
}
function getAlignItems(value: unknown): NonNullable<GridProps['alignItems']> {
if (value === 'start' || value === 'center' || value === 'end' || value === 'baseline') return value;
return 'stretch';
}
function getAutoFlow(value: unknown): NonNullable<GridProps['autoFlow']> {
if (value === 'column' || value === 'row-dense' || value === 'column-dense') return value;
return 'row';
}
function getFrameCount(value: unknown, min: number, max: number, fallback: number) {
const n = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(n)) return fallback;
return Math.min(max, Math.max(min, Math.round(n)));
}
function span(trackCount: number, desired: number) {
return `span ${Math.max(1, Math.min(desired, trackCount))}`;
}
function getSpecs(trackCount: number, frameCount: number) {
if (trackCount <= 2) {
const pattern = [{ minHeight: '4rem' }, { minHeight: '5.5rem' }, { minHeight: '3rem' }];
return [
{ style: { width: '100%', minHeight: '8rem' } as React.CSSProperties },
...Array.from({ length: Math.max(frameCount - 1, 0) }, (_, i) => ({ style: { width: '100%', ...pattern[i % pattern.length] } as React.CSSProperties })),
];
}
const pattern = [
{ width: '100%', minHeight: '3rem' },
{ width: '100%', minHeight: '6.5rem', gridColumn: span(trackCount, 2) },
{ width: '100%', minHeight: '4rem' },
{ width: '100%', minHeight: '3rem' },
];
return [
{ style: { width: '100%', minHeight: '8rem', gridColumn: span(trackCount, Math.max(trackCount - 1, 2)) } as React.CSSProperties },
{ style: { width: '100%', minHeight: '8rem' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as React.CSSProperties },
{ style: { width: '100%', minHeight: '5rem', gridColumn: span(trackCount, 2) } as React.CSSProperties },
{ style: { width: '100%', minHeight: '5rem' } as React.CSSProperties },
...Array.from({ length: Math.max(frameCount - 5, 0) }, (_, i) => ({ style: pattern[i % pattern.length] as React.CSSProperties })),
];
}
export function renderPreview(props: Record<string, unknown>) {
const columns = getColumns(props.columns);
const frameCount = getFrameCount(props.frameCount, 5, 12, 7);
const specs = getSpecs(getApproxColumnCount(columns), frameCount);
return (
<Grid
columns={toColumns(columns)}
gap={getGap(props.gap)}
justifyItems={getJustifyItems(props.justifyItems)}
alignItems={getAlignItems(props.alignItems)}
autoFlow={getAutoFlow(props.autoFlow)}
className="w-full"
>
{specs.map((spec, index) => (
<Frame key={index} pathStroke="dashed" style={{ ...BASE_CELL_STYLE, ...spec.style }} className="w-full h-full">
<div className="size-full" />
</Frame>
))}
</Grid>
);
}
export default function Example() {
return renderPreview({ columns: '4', gap: 'md', justifyItems: 'stretch', alignItems: 'stretch', autoFlow: 'row-dense', frameCount: 7 });
}Responsive Card Rail
Grid that adapts column count and gap based on container width using responsive prop objects.
1024px
"use client";
import React from 'react'
import { Grid, Frame } from 'ui-lab-components'
import type { ControlDef } from '@/types'
type GridColumnsValue = '2' | '3' | '4' | 'auto-fit' | 'auto-fill';
type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const BASE_CELL_STYLE = {
'--frame-fill': 'var(--background-900)',
'--frame-stroke-color': 'var(--background-600)',
} as React.CSSProperties;
export const controls: ControlDef[] = [
{
name: 'columns',
label: 'Columns',
type: 'select',
options: [
{ label: '2 Columns', value: '2' },
{ label: '3 Columns', value: '3' },
{ label: '4 Columns', value: '4' },
{ label: 'Auto Fit', value: 'auto-fit' },
{ label: 'Auto Fill', value: 'auto-fill' },
],
defaultValue: '4',
},
{
name: 'gap',
label: 'Base Gap Token',
type: 'select',
options: [
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
],
defaultValue: 'md',
},
{
name: 'rowGap',
label: 'Row Gap',
type: 'select',
options: [
{ label: 'Match Gap', value: 'inherit' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
],
defaultValue: 'inherit',
},
{
name: 'columnGap',
label: 'Column Gap',
type: 'select',
options: [
{ label: 'Match Gap', value: 'inherit' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
],
defaultValue: 'inherit',
},
{
name: 'frameCount',
label: 'Cards',
type: 'stepper',
defaultValue: 6,
min: 4,
max: 10,
step: 1,
},
{
name: 'responsive',
label: 'Enable Responsive Object',
type: 'toggle',
defaultValue: true,
},
];
export const previewLayout = 'start' as const;
export const resizable = true;
function getColumns(value: unknown): GridColumnsValue {
if (value === '2' || value === '3' || value === 'auto-fit' || value === 'auto-fill') return value;
return '4';
}
function toColumns(value: GridColumnsValue): number | 'auto-fit' | 'auto-fill' {
if (value === 'auto-fit' || value === 'auto-fill') return value;
return Number(value);
}
function getGap(value: unknown): GridGap {
if (value === 'xs' || value === 'sm' || value === 'lg' || value === 'xl') return value;
return 'md';
}
function getOptionalGap(value: unknown): GridGap | undefined {
if (value === 'inherit' || value == null) return undefined;
return getGap(value);
}
function getFrameCount(value: unknown, min: number, max: number, fallback: number) {
const n = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(n)) return fallback;
return Math.min(max, Math.max(min, Math.round(n)));
}
const SPEC_PATTERN = [
{ minHeight: '9rem' },
{ minHeight: '4rem' },
{ minHeight: '3rem' },
{ minHeight: '5rem' },
{ minHeight: '3.5rem' },
{ minHeight: '6rem' },
];
export function renderPreview(props: Record<string, unknown>) {
const columns = getColumns(props.columns);
const baseColumns = toColumns(columns);
const gap = getGap(props.gap);
const responsive = Boolean(props.responsive);
const frameCount = getFrameCount(props.frameCount, 4, 10, 6);
const specs = Array.from({ length: frameCount }, (_, i) => SPEC_PATTERN[i % SPEC_PATTERN.length]);
return (
<Grid
columns={responsive ? { sm: 1, md: 2, lg: baseColumns } : baseColumns}
gap={responsive ? { sm: 'sm', md: gap, lg: gap } : gap}
rowGap={getOptionalGap(props.rowGap)}
columnGap={getOptionalGap(props.columnGap)}
responsive={responsive}
className="w-full"
>
{specs.map((spec, index) => (
<Frame key={index} pathStroke="dashed" style={{ ...BASE_CELL_STYLE, width: '100%', minHeight: spec.minHeight } as React.CSSProperties} className="w-full h-full">
<div className="size-full" />
</Frame>
))}
</Grid>
);
}
export default function Example() {
return renderPreview({ columns: '4', gap: 'md', rowGap: 'inherit', columnGap: 'inherit', frameCount: 6, responsive: true });
}Editorial Spans
Dense auto-flow grid with mixed column spans for editorial content layouts.
"use client";
import type { CSSProperties } from 'react';
import { Grid, Frame } from 'ui-lab-components';
import type { GridProps } from 'ui-lab-components';
type ControlDef = {
name: string;
label: string;
type: 'select' | 'toggle' | 'text' | 'stepper';
options?: Array<{ label: string; value: string | number | boolean }>;
defaultValue?: string | number | boolean;
min?: number;
max?: number;
step?: number;
};
type GridColumnsValue = '1' | '2' | '3' | '4' | '5' | '6' | 'auto-fit' | 'auto-fill';
type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const BASE_CELL_STYLE = {
'--frame-fill': 'var(--background-900)',
'--frame-stroke-color': 'var(--background-600)',
} as CSSProperties;
export const metadata = {
title: 'Editorial Spans',
description: 'Dense auto-flow grid with mixed column spans for editorial content layouts.',
access: 'free' as const,
};
export const controls: ControlDef[] = [
{
name: 'columns',
label: 'Columns',
type: 'select',
options: [
{ label: '1 Column', value: '1' },
{ label: '2 Columns', value: '2' },
{ label: '3 Columns', value: '3' },
{ label: '4 Columns', value: '4' },
{ label: '5 Columns', value: '5' },
{ label: '6 Columns', value: '6' },
{ label: 'Auto Fit', value: 'auto-fit' },
{ label: 'Auto Fill', value: 'auto-fill' },
],
defaultValue: '4',
},
{
name: 'gap',
label: 'Gap Token',
type: 'select',
options: [
{ label: 'Extra Small', value: 'xs' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
],
defaultValue: 'md',
},
{
name: 'justifyItems',
label: 'Inline Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
],
defaultValue: 'stretch',
},
{
name: 'alignItems',
label: 'Block Alignment',
type: 'select',
options: [
{ label: 'Stretch', value: 'stretch' },
{ label: 'Start', value: 'start' },
{ label: 'Center', value: 'center' },
{ label: 'End', value: 'end' },
{ label: 'Baseline', value: 'baseline' },
],
defaultValue: 'stretch',
},
{
name: 'autoFlow',
label: 'Auto Placement',
type: 'select',
options: [
{ label: 'Row', value: 'row' },
{ label: 'Column', value: 'column' },
{ label: 'Row Dense', value: 'row-dense' },
{ label: 'Column Dense', value: 'column-dense' },
],
defaultValue: 'row-dense',
},
{
name: 'frameCount',
label: 'Panels',
type: 'stepper',
defaultValue: 7,
min: 5,
max: 12,
step: 1,
},
];
export const previewLayout = 'start' as const;
export const resizable = true;
function getColumns(value: unknown): GridColumnsValue {
if (value === '1' || value === '2' || value === '4' || value === '5' || value === '6' || value === 'auto-fit' || value === 'auto-fill') return value;
return '3';
}
function toColumns(value: GridColumnsValue): number | 'auto-fit' | 'auto-fill' {
if (value === 'auto-fit' || value === 'auto-fill') return value;
return Number(value);
}
function getApproxColumnCount(value: GridColumnsValue) {
if (value === 'auto-fit' || value === 'auto-fill') return 4;
return Number(value);
}
function getGap(value: unknown): GridGap {
if (value === 'xs' || value === 'sm' || value === 'lg' || value === 'xl') return value;
return 'md';
}
function getJustifyItems(value: unknown): NonNullable<GridProps['justifyItems']> {
if (value === 'start' || value === 'center' || value === 'end') return value;
return 'stretch';
}
function getAlignItems(value: unknown): NonNullable<GridProps['alignItems']> {
if (value === 'start' || value === 'center' || value === 'end' || value === 'baseline') return value;
return 'stretch';
}
function getAutoFlow(value: unknown): NonNullable<GridProps['autoFlow']> {
if (value === 'column' || value === 'row-dense' || value === 'column-dense') return value;
return 'row';
}
function getFrameCount(value: unknown, min: number, max: number, fallback: number) {
const numericValue = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(numericValue)) return fallback;
return Math.min(max, Math.max(min, Math.round(numericValue)));
}
function span(trackCount: number, desired: number) {
return `span ${Math.max(1, Math.min(desired, trackCount))}`;
}
function getSpecs(trackCount: number, frameCount: number) {
if (trackCount <= 2) {
const pattern = [{ minHeight: '4rem' }, { minHeight: '5.5rem' }, { minHeight: '3rem' }];
return [
{ style: { width: '100%', minHeight: '8rem' } as CSSProperties },
...Array.from({ length: Math.max(frameCount - 1, 0) }, (_, index) => ({
style: { width: '100%', ...pattern[index % pattern.length] } as CSSProperties,
})),
];
}
const pattern = [
{ width: '100%', minHeight: '3rem' },
{ width: '100%', minHeight: '6.5rem', gridColumn: span(trackCount, 2) },
{ width: '100%', minHeight: '4rem' },
{ width: '100%', minHeight: '3rem' },
];
return [
{ style: { width: '100%', minHeight: '8rem', gridColumn: span(trackCount, Math.max(trackCount - 1, 2)) } as CSSProperties },
{ style: { width: '100%', minHeight: '8rem' } as CSSProperties },
{ style: { width: '100%', minHeight: '3rem' } as CSSProperties },
{ style: { width: '100%', minHeight: '5rem', gridColumn: span(trackCount, 2) } as CSSProperties },
{ style: { width: '100%', minHeight: '5rem' } as CSSProperties },
...Array.from({ length: Math.max(frameCount - 5, 0) }, (_, index) => ({ style: pattern[index % pattern.length] as CSSProperties })),
];
}
export function renderPreview(props: Record<string, unknown>) {
const columns = getColumns(props.columns);
const frameCount = getFrameCount(props.frameCount, 5, 12, 7);
return (
<Grid
columns={toColumns(columns)}
gap={getGap(props.gap)}
justifyItems={getJustifyItems(props.justifyItems)}
alignItems={getAlignItems(props.alignItems)}
autoFlow={getAutoFlow(props.autoFlow)}
className="w-full"
>
{getSpecs(getApproxColumnCount(columns), frameCount).map((spec, index) => (
<Frame key={index} pathStroke="dashed" style={{ ...BASE_CELL_STYLE, ...spec.style }} className="w-full h-full">
<div className="size-full" />
</Frame>
))}
</Grid>
);
}
export default function Example() {
return renderPreview({ columns: '4', gap: 'md', justifyItems: 'stretch', alignItems: 'stretch', autoFlow: 'row-dense', frameCount: 7 });
}
Responsive Card Rail
Grid that adapts column count and gap based on container width using responsive prop objects.
"use client";
import type { CSSProperties } from 'react';
import { Grid, Frame } from 'ui-lab-components';
type ControlDef = {
name: string;
label: string;
type: 'select' | 'toggle' | 'text' | 'stepper';
options?: Array<{ label: string; value: string | number | boolean }>;
defaultValue?: string | number | boolean;
min?: number;
max?: number;
step?: number;
};
type GridColumnsValue = '2' | '3' | '4' | 'auto-fit' | 'auto-fill';
type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const BASE_CELL_STYLE = {
'--frame-fill': 'var(--background-900)',
'--frame-stroke-color': 'var(--background-600)',
} as CSSProperties;
export const metadata = {
title: 'Responsive Card Rail',
description: 'Grid that adapts column count and gap based on container width using responsive prop objects.',
access: 'free' as const,
};
export const controls: ControlDef[] = [
{
name: 'columns',
label: 'Columns',
type: 'select',
options: [
{ label: '2 Columns', value: '2' },
{ label: '3 Columns', value: '3' },
{ label: '4 Columns', value: '4' },
{ label: 'Auto Fit', value: 'auto-fit' },
{ label: 'Auto Fill', value: 'auto-fill' },
],
defaultValue: '4',
},
{
name: 'gap',
label: 'Base Gap Token',
type: 'select',
options: [
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
],
defaultValue: 'md',
},
{
name: 'rowGap',
label: 'Row Gap',
type: 'select',
options: [
{ label: 'Match Gap', value: 'inherit' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
],
defaultValue: 'inherit',
},
{
name: 'columnGap',
label: 'Column Gap',
type: 'select',
options: [
{ label: 'Match Gap', value: 'inherit' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
],
defaultValue: 'inherit',
},
{
name: 'frameCount',
label: 'Cards',
type: 'stepper',
defaultValue: 6,
min: 4,
max: 10,
step: 1,
},
{
name: 'responsive',
label: 'Enable Responsive Object',
type: 'toggle',
defaultValue: true,
},
];
export const previewLayout = 'start' as const;
export const resizable = true;
function getColumns(value: unknown): GridColumnsValue {
if (value === '2' || value === '3' || value === 'auto-fit' || value === 'auto-fill') return value;
return '4';
}
function toColumns(value: GridColumnsValue): number | 'auto-fit' | 'auto-fill' {
if (value === 'auto-fit' || value === 'auto-fill') return value;
return Number(value);
}
function getGap(value: unknown): GridGap {
if (value === 'xs' || value === 'sm' || value === 'lg' || value === 'xl') return value;
return 'md';
}
function getOptionalGap(value: unknown): GridGap | undefined {
if (value === 'inherit' || value == null) return undefined;
return getGap(value);
}
function getFrameCount(value: unknown, min: number, max: number, fallback: number) {
const numericValue = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(numericValue)) return fallback;
return Math.min(max, Math.max(min, Math.round(numericValue)));
}
const SPEC_PATTERN = [
{ minHeight: '9rem' },
{ minHeight: '4rem' },
{ minHeight: '3rem' },
{ minHeight: '5rem' },
{ minHeight: '3.5rem' },
{ minHeight: '6rem' },
];
export function renderPreview(props: Record<string, unknown>) {
const columns = getColumns(props.columns);
const baseColumns = toColumns(columns);
const gap = getGap(props.gap);
const responsive = Boolean(props.responsive);
const frameCount = getFrameCount(props.frameCount, 4, 10, 6);
return (
<Grid
columns={responsive ? { sm: 1, md: 2, lg: baseColumns } : baseColumns}
gap={responsive ? { sm: 'sm', md: gap, lg: gap } : gap}
rowGap={getOptionalGap(props.rowGap)}
columnGap={getOptionalGap(props.columnGap)}
responsive={responsive}
className="w-full"
>
{Array.from({ length: frameCount }, (_, index) => SPEC_PATTERN[index % SPEC_PATTERN.length]).map((spec, index) => (
<Frame
key={index}
pathStroke="dashed"
style={{ ...BASE_CELL_STYLE, width: '100%', minHeight: spec.minHeight } as CSSProperties}
className="w-full h-full"
>
<div className="size-full" />
</Frame>
))}
</Grid>
);
}
export default function Example() {
return renderPreview({ columns: '4', gap: 'md', rowGap: 'inherit', columnGap: 'inherit', frameCount: 6, responsive: true });
}