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 });
}
 
UI Lab