DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Grid

A CSS-grid layout primitive with static and responsive column counts, gap presets, flow control, and per-cell placement via GridItem. Polymorphic render, RTL, and no hard-coded tokens.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Grid, GridItem } from "@tessinaui/ui";
{/* Fixed 3-column grid */}
<Grid cols={3} gap="md">
  <Card>One</Card>
  <Card>Two</Card>
  <Card>Three</Card>
</Grid>

{/* Responsive columns */}
<Grid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} gap="sm">
  {items.map((item) => <Card key={item.id}>{item.title}</Card>)}
</Grid>

{/* Cell placement with GridItem */}
<Grid cols={4} rows={3} gap="sm">
  <GridItem colSpan={2} rowSpan={2}>
    <Card>Featured</Card>
  </GridItem>
  <Card>1</Card>
  <Card>2</Card>
  <Card>3</Card>
  <Card>4</Card>
</Grid>

{/* Explicit line placement */}
<Grid cols={6}>
  <GridItem colStart={2} colEnd={5}>
    <Hero />
  </GridItem>
</Grid>

{/* Dense flow backfills gaps */}
<Grid cols={4} flow="dense" gap="sm">
  <GridItem colSpan={2}>Wide</GridItem>
  <Card>Auto</Card>
  <Card>Auto</Card>
</Grid>

Examples

Default

A fixed three-column grid with the default md gap.

Responsive columns

One column on a phone through four on a laptop.

Responsive spans

Spans respond too, not just the column count — and a span of 0 removes a cell at that width rather than squeezing it.

Auto-fit columns

minChildWidth fits as many columns as will hold a child that wide, so the count follows the container rather than the viewport.

Gap

The gap scale, including the px hairline, and the two per-axis overrides.

Guides

Hairline rules between cells instead of a gap.

Featured cell

A hero spanning every column, and a featured tile two wide by two tall.

Dense flow

flow="dense" backfills the holes that wide cells leave behind.

Media gallery

Square tiles at a px gap, under a section header the grid does not own.

Stats

Two columns on a phone, four on a laptop — no cards, just rhythm.

Pricing

Equal-width, equal-height columns. alignItems="stretch" is the load-bearing part.

Alignment

Five cards in four columns leaves an orphan row; content alignment decides where it sits.

Horizontal scroll

Two rows flowing sideways, with the next column peeking at the edge.

Template areas

A layout you can read as a picture.

Subgrid

Cards that adopt their parent's rows so their titles and bodies line up.

Order

Visual reordering — and the reason to be careful with it.

Semantic list

render puts the grid on a <ul> and each cell on an <li> — with the explicit list roles WebKit needs once the layout is a grid.

RTL

Tracks follow the writing direction on their own.

Loading

A skeleton grid that holds the same frame as the loaded one.

Columns

Pass a number for a fixed count, or a breakpoint object for responsive layouts.

<Grid cols={3}>…</Grid>
<Grid cols={{ base: 1, sm: 2, md: 3, lg: 4, xl: 6 }}>…</Grid>
<Grid cols="subgrid">…</Grid>

Supported column counts: 1 through 12, plus "subgrid". Supported breakpoints: base, sm, md, lg, xl.

Keys are forward-filled: { base: 1, lg: 3 } is one column below lg and three from lg up, with no gap in the middle. An object with no base key leaves the widths below its first key unset — occasionally what you want, usually not.

How a responsive value is emitted

Every rung carries its own ceiling — max-sm: · sm:max-md: · md:max-lg: · lg:max-xl: · xl: — so no viewport width matches two of them. A plain min-width ladder (sm:grid-cols-2 md:grid-cols-3) leaves the winner to stylesheet emission order, which is not something a component can guarantee. You do not have to think about this to use the prop; it is why the class list is longer than you might expect.

Auto-fit columns

minChildWidth replaces cols with a track that repeats as often as the container allows:

<Grid minChildWidth="16rem">…</Grid>
<Grid minChildWidth="16rem" autoRepeat="fit">…</Grid>

"fill" (the default) keeps each column at its natural width and leaves the remainder empty. "fit" stretches the columns that exist to consume it. Reach for this when the right column count is a function of available space rather than of device size.

Rows

ValueCSS mapping
1–6grid-rows-1 – grid-rows-6
"subgrid"grid-rows-subgrid — adopt the parent's rows
{ base, sm, … }Responsive, banded as above

Explicit row counts are usually only needed with rowSpan, rowStart/rowEnd, or a column flow.

A subgrid must be a grid item: one wrapper element between it and its parent and it inherits nothing. Use GridItem render={<Grid rows="subgrid" />} to keep them the same element.

Gap

Eight presets on Tailwind's spacing scale.

ValueTailwindpx
"px"gap-px1
"none"gap-00
"xs"gap-14
"sm"gap-28
"md" (default)gap-416
"lg"gap-624
"xl"gap-832
"2xl"gap-1248

rowGap and columnGap accept the same values and override gap on their axis. All three accept a breakpoint object.

Guides

guides draws a hairline rule between cells and zeroes the gap:

<Grid cols={3} guides>…</Grid>

The rule is a border on each cell, not a painted channel behind the grid — a background would show through as a solid block wherever the last row runs short. The container clips the rules that reach its own edge, so only the internal grid is drawn; wrap it in a bordered element if you want an outer frame too. guides and the gap props are mutually exclusive, and setting both logs a development warning.

Flow and implicit tracks

ValueCSS mapping
"row"grid-flow-row — fill row by row (default)
"col"grid-flow-col — fill column by column
"dense"grid-flow-dense — backfill gaps with smaller items
"row-dense"grid-flow-row-dense
"col-dense"grid-flow-col-dense

autoCols and autoRows size the tracks a flow creates implicitly — "auto", "min", "max" or "fr". A two-row horizontal scroller is rows={2} flow="col" autoCols="max".

Alignment

PropCSSValues
alignItemsalign-itemsstart / center / end / stretch / baseline
justifyItemsjustify-itemsstart / center / end / stretch
alignContentalign-contentstart / center / end / stretch
justifyContentjustify-contentstart / center / end / stretch

Template areas

<Grid areas={["head head", "side main"]} cols={2}>
  <GridItem area="head">…</GridItem>
  <GridItem area="side">…</GridItem>
  <GridItem area="main">…</GridItem>
</Grid>

Each string is one row; quotes are added for you.

Two things to know. The template sets the track count, so a cols prop alongside it has nothing left to decide — a two-area row is a two-column grid whatever cols says. And areas is not responsive: it is one inline grid-template-areas value at every width. When the shape has to change at a breakpoint, use cols with responsive spans instead.

Areas also move cells visually without moving them in the DOM — see Accessibility below.

Inline

inline={true} swaps grid for inline-grid so the container flows alongside surrounding inline content.

Polymorphic render

Clone a semantic element while keeping Grid styling. The classes and data-slot land on your element; no wrapper is inserted — which matters, because a wrapper between a grid and its cell makes the cell's placement inert.

<Grid render={<ul aria-label="Tiles" />} cols={3} gap="md">
  <GridItem render={<li />}>One</GridItem>
  <GridItem render={<li />}>Two</GridItem>
</Grid>

A render host keeps its own ref and its own data-slot; both are merged rather than replaced.

GridItem

Per-cell placement and alignment. GridItem is optional — you can always apply col-span-* / row-span-* directly with className.

colSpan / rowSpan

ValueCSS
"auto"col-auto / row-auto
"full"col-span-full / row-span-full
1–12col-span-n / row-span-n
0Hides the cell — only inside a breakpoint object
{ base, sm, … }Responsive, banded as above

colStart / colEnd / rowStart / rowEnd

Explicit grid-line placement, "auto" or 1–13. colStart also accepts a breakpoint object, which is how you express a responsive offset.

<GridItem colStart={2} colEnd={5}>…</GridItem>
<GridItem colStart={{ base: "auto", md: 2 }}>…</GridItem>

order

"none" · "first" · "last" · 1–12. Visual only — see Accessibility.

area

The name of an area declared in the parent's areas.

alignSelf / justifySelf

Per-cell override of the parent's alignItems / justifyItems.

<GridItem alignSelf="end" justifySelf="center">…</GridItem>

RTL

dir="rtl" is forwarded to the DOM node, and CSS grid does the rest: columns lay out right-to-left and colStart counts from the right. The component contains no ltr: / rtl: variants — those compile to a :lang() or [dir]-descendant test that matches both ways round once a grid is nested inside another direction.

Accessibility

  • Grid is a structural primitive — it applies no ARIA roles.
  • Use render to mount semantic HTML (<ul>, <section>, <main>) when the grouping has meaning, so the structure is announced rather than inferred.
  • A <ul> laid out as a grid needs role="list", and its cells role="listitem". The roles look redundant and are not: grid layout makes the children display: flex, and WebKit drops the implicit list semantics of any <li> that is no longer display: list-item. Without them VoiceOver announces the example above as "Departments group" and reads the cells as loose text with no "1 of 6". axe passes it either way, so this is one to take on trust rather than on a green gate.
  • Visual order is not DOM order. order, areas, colStart and rowStart all move cells on screen without moving them in the markup, and screen readers and the Tab key follow the markup. Where the sequence carries meaning, reorder the markup too. Shopify deprecated its own areas API over exactly this mismatch; ours keeps it, and puts the responsibility here.
  • A cell hidden with colSpan={{ base: 0 }} is display: none, so it is removed from the accessibility tree as well as the layout. That is usually right for decorative cells and wrong for content — if the content matters at every width, reflow it instead of hiding it.
  • guides draws decoration, not structure: it carries no semantics and needs none.

When to use Grid vs. Flex

Use Grid when…Use Flex when…
You have a two-dimensional layoutYou have a one-dimensional layout
Items should align on both axesOnly the main axis matters
You need equal-width columns without basis mathItem widths vary by content
You want explicit cell placement (colStart, rowSpan)Items flow naturally

A list/grid view toggle is two layouts, not one: swap Grid for Stack or Flex and keep the container's padding identical so the switch does not shift the page.

API Reference

Grid Props

PropTypeDefaultDescription
cols1..12 | "subgrid", or a breakpoint object—Column count
rows1..6 | "subgrid", or a breakpoint object—Row count
gap"px" | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl", or a breakpoint object"md"Both-axis gap
rowGapSame as gap—Block-axis gap (overrides gap)
columnGapSame as gap—Inline-axis gap (overrides gap)
minChildWidthstring—Fit as many columns as hold a child this wide; replaces cols
autoRepeat"fill" | "fit""fill"How minChildWidth repeats
autoCols"auto" | "min" | "max" | "fr"—grid-auto-columns
autoRows"auto" | "min" | "max" | "fr"—grid-auto-rows
guidesbooleanfalseHairline rules between cells; zeroes the gap
areasstring[]—grid-template-areas, one string per row
flow"row" | "col" | "dense" | "row-dense" | "col-dense"—grid-auto-flow
alignItems"start" | "center" | "end" | "stretch" | "baseline"—align-items
justifyItems"start" | "center" | "end" | "stretch"—justify-items
alignContent"start" | "center" | "end" | "stretch"—align-content
justifyContent"start" | "center" | "end" | "stretch"—justify-content
inlinebooleanfalseUse inline-grid
renderReactElement—Polymorphic target
dir"ltr" | "rtl"—Text direction
classNamestring—Extra classes

GridItem Props

PropTypeDefaultDescription
colSpan"auto" | "full" | 1..12, or a breakpoint object (0 allowed)—grid-column: span n
rowSpan"auto" | "full" | 1..12, or a breakpoint object—grid-row: span n
colStart"auto" | 1..13, or a breakpoint object—grid-column-start
colEnd"auto" | 1..13—grid-column-end
rowStart"auto" | 1..13—grid-row-start
rowEnd"auto" | 1..13—grid-row-end
order"none" | "first" | "last" | 1..12—Visual order only
areastring—Named area from the parent's areas
alignSelf"auto" | "start" | "center" | "end" | "stretch" | "baseline"—align-self
justifySelf"auto" | "start" | "center" | "end" | "stretch"—justify-self
renderReactElement—Polymorphic target
dir"ltr" | "rtl"—Text direction
classNamestring—Extra classes

Notes

  • No intrinsic padding. Grid controls layout only — add padding, borders and background via className, or compose with Card or Surface.
  • Data attributes. The container carries data-slot="grid" and each cell data-slot="grid-item", so tests and agents have a stable hook. A render host that names its own slot keeps it.
  • No SSR caveats. Grid has no hooks beyond ref merging; it renders on the server.
  • Static class names. Every class is a literal in the source, so Tailwind's scanner sees them all. Interpolating a class name — or a variant prefix — would compile to nothing.
  • Masonry is out of scope. A staggered, variable-height wall is not CSS grid: no surveyed design system implements it with one, and neither do we. Use CSS columns (className="columns-2 gap-4" with break-inside-avoid children) when you want that look, and Grid when you want cells that line up.
  • Arbitrary values. For anything outside the presets, drop to className="grid-cols-[repeat(3,8rem)]". Pass the whole ladder yourself if it is responsive — mixing a bare utility with a breakpoint variant of the same property is decided by emission order.
FormHoverCard

On this page

PlaygroundInstallationUsageExamplesDefaultResponsive columnsResponsive spansAuto-fit columnsGapGuidesFeatured cellDense flowMedia galleryStatsPricingAlignmentHorizontal scrollTemplate areasSubgridOrderSemantic listRTLLoadingColumnsHow a responsive value is emittedAuto-fit columnsRowsGapGuidesFlow and implicit tracksAlignmentTemplate areasInlinePolymorphic renderGridItemcolSpan / rowSpancolStart / colEnd / rowStart / rowEndorderareaalignSelf / justifySelfRTLAccessibilityWhen to use Grid vs. FlexAPI ReferenceGrid PropsGridItem PropsNotes