Skip to content
Primitiv home
Framework
Consumption mode

Grid

stableSource Figma

A CSS Grid container. columns takes a count or a mobile-first per-breakpoint map, gap the spacing (a density-scaled Context token per step), and align/justify how each item sits within its cell. No inline styles: the per-breakpoint column counts resolve to modifier classes, so the responsive behaviour is correct in server-rendered markup on the first paint.

Playground

Density

Preview

One
Two
lines
Three
Four
Five
lines
Six
Columns
Gap
Align
Justify
import { Grid } from "@/components/ui/grid";
<Grid  columns={1}  gap="md"  align="stretch"  justify="stretch">  <div>One</div>  <div>Two</div>  <div>Three</div>  {/* ... */}</Grid>

Density is set by a data-density ancestor — the Context system, not a Grid prop.

Installation

npx primitiv add grid

Import

import { Grid } from "@/components/ui/grid";

No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.

Props

Grid

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultDescription
asChildbooleanRender the single child element instead of a wrapping <div>, merging the grid classes onto it — e.g. <Grid asChild><ul>...</ul></Grid>.
columns"1" | "2" | "3" | "4" | "5" | "6"1The column count. The options below are the base tier, applied at every width. Each is also available per breakpoint as primitiv-grid--{tier}-cols-{n}, where tier is one of xs|sm|md|lg|xl|2xl — e.g. primitiv-grid--md-cols-2. Tiers are mobile-first floors, not ranges, and the stylesheet orders them ascending so the widest matching tier wins. A count outside 1–6, or a tier outside this scale, is a media query of your own against --primitiv-grid-columns.
gap"none" | "xs" | "sm" | "md" | "lg" | "xl"mdSpace between tracks; each step (except none) is a density-scaled Context token, mirroring Stack's gap ramp step for step.
align"start" | "center" | "end" | "stretch" | "baseline"stretchHow each item sits within its cell on the block axis (align-items), as a preset set rather than a raw CSS passthrough.
justify"start" | "center" | "end" | "stretch"stretchThe inline-axis equivalent (justify-items). Grid distributes tracks itself, so there is no between/around/evenly here the way there is on Stack's flex justify-content.

Styling contract

--primitiv-grid-columns--primitiv-grid-gap

Accessibility

  • Grid renders a plain <div> — visual layout only, no semantics. When the items are a list, use asChild to render a <ul> so the count and structure are announced rather than flattened.
  • The visual grid does not change DOM order, so keyboard focus and screen-reader reading order follow the source. Avoid using grid placement to reorder items away from a sensible reading order.
  • Tracks are minmax(0, 1fr), so a cell with long unbreakable content is clipped by its track rather than forcing the whole row wider — which keeps the layout (and horizontal scrolling) predictable for everyone.

Examples

Responsive columns

The reason Grid exists rather than a raw grid-template-columns: columns takes a mobile-first map, one column on the smallest screens escalating to more as space allows. Because each tier is a modifier class, not an inline style, the right column count is in the server-rendered HTML on the first paint — no flash of the wrong layout, no useEffect measuring the viewport.

Density
One
Two
Three
Four
Five
Six
import { Grid } from "@/components/ui/grid";
<Grid columns={{ base: 1, md: 2, lg: 3 }} gap="lg">  <Card />  <Card />  <Card /></Grid>

Aligning items in their cells

align (block axis) and justify (inline axis) set how each item sits inside its own cell. Both default to stretch, so items fill the cell — the usual choice for cards. Switch either to start/center/end when the items are smaller than their cells and should pin to an edge, like a row of controls of different sizes lined up along the same baseline.

Density
One
Two
lines
Three
import { Grid } from "@/components/ui/grid";
<Grid columns={3} gap="md" justify="center" align="center">  <div>One</div>  <div>Two</div>  <div>Three</div></Grid>