Skip to content

Grid

Grid is a generic, presentational layout primitive built on CSS Grid. It arranges arbitrary children into equal or custom-width columns, with gap control, responsive reflow, auto-fit sizing, and optional cell spanning via Grid.Item. It makes no assumptions about your data — children simply flow into the tracks you define.

Grid vs. Layout: Use Layout for top-level page scaffolding (header + fixed-width sidebars + main content). Use Grid for content grids within a page — KPI cards, galleries, field groups, dashboards. They are complementary; reach for Grid whenever you just need "arrange these N things into columns."

Import

tsx
import { Grid } from "@tailor-platform/app-shell";

Basic Usage

tsx
<Grid columns={3} gap={4}>
  <Card.Root>One</Card.Root>
  <Card.Root>Two</Card.Root>
  <Card.Root>Three</Card.Root>
</Grid>

Plain children occupy a single cell. Wrap a child in Grid.Item only when it needs to span multiple tracks or be placed explicitly.

Sub-components

Sub-componentDescription
GridThe grid container — defines columns, rows, gap, and alignment
Grid.ItemAn optional cell that spans multiple columns/rows or is placed by line

Props

Grid Props

PropTypeDefaultDescription
columnsnumber | string | Responsive<number | string>-A number creates that many equal columns; a string is a verbatim grid-template-columns value
rowsnumber | string | Responsive<number | string>-Same shape as columns, for grid-template-rows
gapnumber | Responsive<number>3 (12px)Gap between rows and columns, in spacing-scale units (41rem)
gapXnumber | Responsive<number>-Horizontal (column) gap override
gapYnumber | Responsive<number>-Vertical (row) gap override
minChildWidthnumber | string-Auto-fit: each item is at least this wide and as many as fit are placed per row. Overrides columns
flow"row" | "column" | "dense" | "row-dense" | "column-dense""row"grid-auto-flow — placement direction and dense packing
align"start" | "center" | "end" | "stretch" | "baseline"-align-items — block-axis alignment of items within their tracks
justify"start" | "center" | "end" | "between" | "around" | "evenly"-justify-content — inline-axis distribution of tracks within the grid
classNamestring-Additional CSS classes
childrenReact.ReactNode-Grid items (any elements, or Grid.Item)

All standard HTML <div> props are also accepted.

Grid.Item Props

PropTypeDefaultDescription
colSpannumber | "full" | Responsive<number | "full">-Columns to span, or "full" for all columns
rowSpannumber | "full" | Responsive<number | "full">-Rows to span, or "full" for all rows
colStartnumber | Responsive<number>-1-based column line to start at (grid-column-start)
colEndnumber | Responsive<number>-1-based column line to end at (grid-column-end)
classNamestring-Additional CSS classes
childrenReact.ReactNode-Cell content

All standard HTML <div> props are also accepted.

Responsive values

columns, rows, gap, gapX, gapY, and every Grid.Item prop accept either a single value (applied at all breakpoints) or a breakpoint object. Breakpoints align with Tailwind: initial (base), sm, md, lg, xl, 2xl.

tsx
// 1 column on mobile, 2 from md, 4 from xl
<Grid columns={{ initial: 1, md: 2, xl: 4 }} gap={4}>
  {/* … */}
</Grid>

Sizing columns

There are three ways to size columns; pick whichever fits.

tsx
// 1. Equal columns
<Grid columns={4} gap={4}>{/* … */}</Grid>

// 2. Custom track widths (verbatim grid-template-columns)
<Grid columns="280px 1fr" gap={6}>{/* … */}</Grid>

// 3. Auto-fit — as many ≥240px columns as fit, no breakpoints needed
<Grid minChildWidth={240} gap={4}>{/* … */}</Grid>

minChildWidth accepts a number (pixels) or a string (e.g. "16rem") and takes precedence over columns when set.

Spanning with Grid.Item

tsx
<Grid columns={4} gap={4}>
  <Grid.Item colSpan={2}>Wide card</Grid.Item>
  <Card.Root>Normal</Card.Root>
  <Grid.Item colSpan={{ initial: "full", md: 1 }} rowSpan={2}>
    Tall
  </Grid.Item>
  <Grid.Item colSpan="full">Full-width footer row</Grid.Item>
</Grid>

Alignment

align aligns items within their row (visible when row height exceeds the item), justify distributes the columns when they are narrower than the container (e.g. fixed-width tracks), and flow controls placement order:

tsx
<Grid columns="repeat(3, 96px)" gap={4} justify="between" align="center">
  {/* fixed-width columns spread across the container */}
</Grid>

Note: justify only has a visible effect when the tracks do not fill the container. With fluid columns={n} (which uses 1fr tracks) the columns always fill the width, so justify is a no-op — use fixed-width tracks to distribute columns. Likewise, flow="…-dense" only differs from the default when items span multiple cells and leave gaps to backfill.

Examples

Responsive KPI grid

tsx
<Grid columns={{ initial: 1, sm: 2, lg: 4 }} gap={4}>
  {metrics.map((m) => (
    <MetricCard key={m.id} title={m.label} value={m.value} />
  ))}
</Grid>
tsx
<Grid minChildWidth={240} gap={6}>
  {products.map((p) => (
    <Card.Root key={p.sku}>
      <Card.Header title={p.name} description={p.sku} />
    </Card.Root>
  ))}
</Grid>
tsx
<Grid columns="280px 1fr" gap={6}>
  <Card.Root>{/* filters */}</Card.Root>
  <Card.Root>{/* results */}</Card.Root>
</Grid>
tsx
<Grid columns={{ initial: 1, md: 4 }} gap={4}>
  <Grid.Item colSpan={{ initial: "full", md: 2 }}>
    <Card.Root>Featured</Card.Root>
  </Grid.Item>
  <Card.Root>Stat</Card.Root>
  <Card.Root>Stat</Card.Root>
</Grid>

Styling

Grid is a presentational primitive: it renders a single <div data-slot="grid"> (and <div data-slot="grid-item"> for cells) styled with Tailwind utilities prefixed astw:. Dynamic templates are applied via CSS custom properties, so arbitrary column counts and responsive values work without extra configuration. Pass className or style to extend or override.

  • Layout - Page-level scaffold (header + sidebars + main)
  • Card - Common child of a grid cell
  • MetricCard - KPI cards for dashboard grids