Skip to main contentSkip to docs navigation

Responsive Grid

Build responsive layouts with the mobile-first CSS grid — twelve columns, six breakpoints, a gutter per breakpoint, and responsive spans and column counts.

Overview

The grid system lays content out on twelve columns with <Grid> and <GridItem>. It is built on CSS Grid and follows the viewport through six breakpoints.

One of three columns
One of three columns
One of three columns
JSX
import { Container, Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Container>
      <Grid>
        <GridItem span={4}>One of three columns</GridItem>
        <GridItem span={4}>One of three columns</GridItem>
        <GridItem span={4}>One of three columns</GridItem>
      </Grid>
    </Container>
  )
}

Three items of four tracks each make three equal columns at every viewport width. The <Container> around them centers the grid in the page; a grid doesn't need one to work.

This guide used to describe the flexbox grid of <Row> and <Col>, which @chassis-ui/react 0.4 removes. See Migrating from Row/Col to move a layout over.

Layout primitives

The grid isn't the only layout tool, and two others cover what it isn't the best fit for:

  • <Stack> for a single row or column of items, like a button toolbar or the fields of a form.
  • <Flex> for items as wide as their content, and for wrapping, justification, and alignment along one axis.

Reach for <Grid> when items share column tracks: page layouts, card grids, and forms whose fields line up across rows.

How it works

The grid system combines breakpoints, containers, a grid, and its items into one layout model:

  • The grid follows six breakpoints. Breakpoints are min-width media queries, so a value set for one applies to it and to every wider one: span={{ base: 'full', sm: 4 }} is four tracks from sm up, until another key overrides it.

  • Containers center the page and pad its sides. Use <Container> for a width per breakpoint, <Container fluid> for the full width, or <Container fluidUntil="md"> for a mix. The padding of a container is the page margin, separate from the gutter of the grid.

  • A grid has twelve columns. <Grid> divides its width into equal tracks. columns changes the count, for the whole grid or from a breakpoint up.

  • Items span tracks. <GridItem span={4}> takes four of the twelve. An item with no span is one track wide, so an item that should fill the row takes span="full".

  • The gutter follows the breakpoint. With no gap, the space between items is the gutter of the current breakpoint, 1rem below md and 1.5rem from it, in both directions. gap sets another, for the whole grid or from a breakpoint up.

Grid options

The grid adapts across six breakpoints. A layout prop takes one value for every width, or an object with a value per breakpoint, each a key of that object. See Grid options on the CSS site for the width, gutter, and page margin of each.

BreakpointKey
Extra small

base: the value from the narrowest width up

Smallsm
Mediummd
Largelg
Extra largexl
Extra extra large2xl

Equal columns

Columns of the same width need no span on each item: the grid counts them for you, or you give it the count.

As many as children

<Grid fill> gives every child a column of the same width, however many there are. Add min to wrap the children once a column would get narrower than that.

1 of 2
2 of 2
1 of 3
2 of 3
3 of 3
JSX
import { Grid } from '@chassis-ui/react'

export const Example = () => {
  return (
    <>
      <Grid fill>
        <div>1 of 2</div>
        <div>2 of 2</div>
      </Grid>
      <Grid fill>
        <div>1 of 3</div>
        <div>2 of 3</div>
        <div>3 of 3</div>
      </Grid>
    </>
  )
}

A count per breakpoint

columns sets how many equal columns a grid has, and an object changes the count as the viewport widens: columns={{ base: 1, sm: 2, lg: 4 }}. Further children wrap to the next row. This is what <Row cols> did.

Column
Column
Column
Column
Column
Column
Column
Column
Column
Column
JSX
import { Grid } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Grid columns={{ base: 2, md: 3, lg: 5 }} gap={{ base: 'sm', lg: 'md' }}>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
      <div>Column</div>
    </Grid>
  )
}

Spans

On the twelve default columns, span gives each item its own width.

All breakpoints

A span with one value holds from the narrowest viewport to the widest.

span=3
span=3
span=3
span=3
span=8
span=4
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <>
      <Grid>
        <GridItem span={3}>span=3</GridItem>
        <GridItem span={3}>span=3</GridItem>
        <GridItem span={3}>span=3</GridItem>
        <GridItem span={3}>span=3</GridItem>
      </Grid>
      <Grid>
        <GridItem span={8}>span=8</GridItem>
        <GridItem span={4}>span=4</GridItem>
      </Grid>
    </>
  )
}

Stacked to horizontal

Give the items 'full' as the base of their span and a narrower span from a breakpoint up, as in span={{ base: 'full', md: 4 }}, and they start out stacked and sit side by side from that breakpoint. A column count does the same for equal columns: 1 as the base, more from the breakpoint.

span=full sm:span=8
span=full sm:span=4
sm:columns=3
sm:columns=3
sm:columns=3
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <>
      <Grid>
        <GridItem span={{ base: 'full', sm: 8 }}>span=full sm:span=8</GridItem>
        <GridItem span={{ base: 'full', sm: 4 }}>span=full sm:span=4</GridItem>
      </Grid>
      <Grid columns={{ base: 1, sm: 3 }}>
        <div>sm:columns=3</div>
        <div>sm:columns=3</div>
        <div>sm:columns=3</div>
      </Grid>
    </>
  )
}

Mix and match

Give a span a base narrower than the row and other spans from a breakpoint up, so that items share a row at every width, in different proportions.

span=full md:span=8
span=6 md:span=4
span=6 md:span=4
span=6 md:span=4
span=6 md:span=4
span=6
span=6
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <>
      <Grid>
        <GridItem span={{ base: 'full', md: 8 }}>span=full md:span=8</GridItem>
        <GridItem span={{ base: 6, md: 4 }}>span=6 md:span=4</GridItem>
      </Grid>
      <Grid>
        <GridItem span={{ base: 6, md: 4 }}>span=6 md:span=4</GridItem>
        <GridItem span={{ base: 6, md: 4 }}>span=6 md:span=4</GridItem>
        <GridItem span={{ base: 6, md: 4 }}>span=6 md:span=4</GridItem>
      </Grid>
      <Grid>
        <GridItem span={6}>span=6</GridItem>
        <GridItem span={6}>span=6</GridItem>
      </Grid>
    </>
  )
}

Start lines

start puts an item at a column line and leaves the tracks before it empty, which is how you indent or center an item. Unlike the offset of <Col>, a start line counts from the edge of the grid, not from the item before. Don't center a grid item with an auto margin: it shrinks the item to its content.

span=4
span=4 start=9
span=full md:span=6 md:start=4
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <>
      <Grid>
        <GridItem span={4}>span=4</GridItem>
        <GridItem span={4} start={9}>
          span=4 start=9
        </GridItem>
      </Grid>
      <Grid>
        <GridItem span={{ base: 'full', md: 6 }} start={{ md: 4 }}>
          span=full md:span=6 md:start=4
        </GridItem>
      </Grid>
    </>
  )
}

Gaps

The gap is the space between items, set on the grid and not on the items. There are no negative margins, so a grid never overflows its container the way a row with wide gutters did.

Both axes

gap takes a token of the spacing scale and applies it between columns and between rows. An object changes it from a breakpoint up: gap={{ base: 'sm', lg: 'xl' }}.

gap=sm lg:gap=xl
gap=sm lg:gap=xl
gap=sm lg:gap=xl
gap=sm lg:gap=xl
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Grid gap={{ base: 'sm', lg: 'xl' }}>
      <GridItem span={6}>gap=sm lg:gap=xl</GridItem>
      <GridItem span={6}>gap=sm lg:gap=xl</GridItem>
      <GridItem span={6}>gap=sm lg:gap=xl</GridItem>
      <GridItem span={6}>gap=sm lg:gap=xl</GridItem>
    </Grid>
  )
}

One axis

The .column-gap-{token} and .row-gap-{token} utilities set one axis each, in place of the gutterX and gutterY of <Row>. row-gap-zero suits items that carry a bottom margin of their own.

column-gap-xl row-gap-2xs
column-gap-xl row-gap-2xs
column-gap-xl row-gap-2xs
column-gap-xl row-gap-2xs
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Grid className="column-gap-xl row-gap-2xs">
      <GridItem span={6}>column-gap-xl row-gap-2xs</GridItem>
      <GridItem span={6}>column-gap-xl row-gap-2xs</GridItem>
      <GridItem span={6}>column-gap-xl row-gap-2xs</GridItem>
      <GridItem span={6}>column-gap-xl row-gap-2xs</GridItem>
    </Grid>
  )
}

No gap

gap="zero" removes the space on both axes, for items that should touch.

span=full sm:span=6 md:span=8
span=6 md:span=4
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Grid gap="zero">
      <GridItem span={{ base: 'full', sm: 6, md: 8 }}>span=full sm:span=6 md:span=8</GridItem>
      <GridItem span={{ base: 6, md: 4 }}>span=6 md:span=4</GridItem>
    </Grid>
  )
}

Content-width columns

A grid sizes its tracks from the container, not from the content. For items as wide as their content beside items that take the rest of the row, use a <Flex> with a gap, and .flex-fill on the items that grow.

Fills the row
As wide as its content
Fills the row
JSX
import { Flex } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Flex gap="md">
      <div className="flex-fill">Fills the row</div>
      <div>As wide as its content</div>
      <div className="flex-fill">Fills the row</div>
    </Flex>
  )
}

Nested grids

A <Grid> inside an item is a grid of its own, with twelve tracks across the width of the item. The default gutter follows the viewport, not the width of the grid, so in a nested grid — or one in a sidebar or a card — it can be too wide for twelve tracks. Give such a grid a gap and a column count that fits, or let it follow the width of its query container with contained.

Nested column
Nested column
span=full md:span=4
JSX
import { Grid, GridItem } from '@chassis-ui/react'

export const Example = () => {
  return (
    <Grid gap="md">
      <GridItem span={{ base: 'full', md: 8 }}>
        <Grid columns={2} gap="sm">
          <div>Nested column</div>
          <div>Nested column</div>
        </Grid>
      </GridItem>
      <GridItem span={{ base: 'full', md: 4 }}>span=full md:span=4</GridItem>
    </Grid>
  )
}

To align nested items with the outer columns instead, make the item a subgrid.

API

Container

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
fluid?boolean—
Set container 100% wide, spanning the entire width of the viewport.
fluidUntil?Breakpoint—
Set container 100% wide until the given breakpoint, after which it scales up with max-width at every larger breakpoint.
ref?any—

Grid

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
columns?Responsive<number, Breakpoint | ContainerBreakpoint>—
Number of columns in the grid template (the column count of @chassis-ui/css when omitted, 12 by default). A count from 1 to 12 is mapped to the grid-cols-{n} class, which applies to this grid alone. Any other count has no class and is set via the --cx-grid-columns custom property, which a grid nested in this one inherits: give that grid columns of its own. Has no effect when fill is set. An object sets the count from a width up, as in { base: 1, md: 3 }, here and on rows, gap and flow. A breakpoint key (md) is a width of the viewport. A container key ('@md') is the same width of the nearest query container (an ancestor with the contains-inline class), and wins over a breakpoint key where both apply. Without a query container above the grid, a container key never applies. The count of a key is a class, so a count from 1 to 12.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
contained?boolean—
Adds the contained class: the default gutter and column count follow the width of the nearest query container instead of the viewport, so a grid in a narrow column of a wide page has the gutter of a narrow page. Without a query container above it the grid keeps the values of the viewport. columns and gap still override them.
fill?boolean—
Renders .grid-fill instead of .grid — columns expand equally to fill the available width, with the column count determined by the number of children rather than columns.
flow?Responsive<GridFlow, Breakpoint | ContainerBreakpoint>—
The direction items are placed in, mapped to the grid-flow-* classes: by 'row' (the CSS default) or by 'column', which fills the rows of one column before it starts the next. 'dense', 'row-dense' and 'column-dense' also move later items into gaps that earlier, wider ones left. An object sets the flow from a width up.
gap?{ Spacing | string | { base?: Spacing | string } & Partial<Record<Breakpoint | ContainerBreakpoint, Spacing>> }—
Gap between grid items: a Spacing token (mapped to the gap-{token} class) or any raw CSS gap value, including a "{row} {column}" pair (set via the --cx-grid-gap custom property). The gutter of the current breakpoint when omitted, or of the query container when contained is set. An object sets the gap from a width up: only a token fits a key other than base, since a raw value has no class.
min?string—
Minimum column width of a fill grid, set via the --cx-grid-min custom property: any CSS length, e.g. "16rem". Children wrap to a new row when they would get narrower. 12rem in @chassis-ui/css when omitted. Only relevant when fill is set.
ref?any—
rows?Responsive<number, Breakpoint | ContainerBreakpoint>—
Number of equal rows in the grid template. A grid has none when omitted: its rows are as tall as their content. A count from 1 to 6 is mapped to the grid-rows-{n} class; any other count has no class and is set inline, as the grid-template-rows the class declares. Has no effect when fill is set. An object sets the count from a width up, where it is a class, so a count from 1 to 6.

GridItem

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
end?{ Responsive<number | 'auto', Breakpoint | ContainerBreakpoint> }—
Grid column line to end this item at, mapped to the col-end-{n} class (1 to 13, the line after the last of 12 columns). With span, it places the item against the end edge of the grid: span={3} end={13}. 'auto' (col-end-auto) undoes an end line at a wider breakpoint: { base: 13, md: 'auto' }.
gap?{ Spacing | string }—
Gap between the subgrid's children: a Spacing token (mapped to the gap-{token} class) or any raw CSS gap value (set via the --cx-grid-gap custom property). Only relevant when subgrid is set. A subgrid has the gap of its own: when the parent <Grid> takes a gap token, give the subgrid the same one, or its children are narrower than the parent's tracks.
ref?any—
rowEnd?{ Responsive<number | 'auto', Breakpoint | ContainerBreakpoint> }—
Grid row line to end this item at, mapped to the row-end-{n} class (1 to 7, the line after the last of 6 rows), or 'auto' (row-end-auto) to undo an end line at a wider breakpoint. Takes an object per width, as span does.
rows?number—
Number of equal rows in the subgrid's own row template: the grid-rows-{n} class for a count from 1 to 6, the same grid-template-rows set inline for any other. Only relevant when subgrid is set — subgrid only inherits the parent's column tracks, not its rows.
rowSpan?Responsive<number, Breakpoint | ContainerBreakpoint>—
Number of grid row tracks this item spans, mapped to the row-span-{n} class. Takes an object per width, as span does.
rowStart?{ Responsive<number | 'auto', Breakpoint | ContainerBreakpoint> }—
Grid row line to start this item at, mapped to the row-start-{n} class (1 to 6), or 'auto' (row-start-auto) to return it to the flow. Takes an object per width, as span does.
span?{ Responsive<number | 'full', Breakpoint | ContainerBreakpoint> }—
Number of grid column tracks (of the parent <Grid>'s columns) this item spans, mapped to the col-span-{n} class, or 'full' for every track of the row (col-span-full). An item with no span is one track wide. An object sets the span from a width up, as in { base: 'full', md: 6 }, here and on start, end, rowSpan, rowStart and rowEnd. A breakpoint key (md) is a width of the viewport. A container key ('@md') is the same width of the nearest query container (an ancestor with the contains-inline class), and wins over a breakpoint key where both apply. Without a query container above the grid, a container key never applies.
start?{ Responsive<number | 'auto', Breakpoint | ContainerBreakpoint> }—
Grid column line to start this item at, mapped to the col-start-{n} class (1 to 12). 'auto' (col-start-auto) returns the item to the flow, to undo a start line at a wider breakpoint: { base: 4, md: 'auto' }.
subgrid?boolean—
Turns this item into a nested subgrid: adds .grid/.grid-cols-subgrid alongside its col-span-{n}/col-start-{n} placement classes, so its own children inherit the parent <Grid>'s column tracks instead of defining new ones. Combine with span — a subgrid item must itself be a grid item of the parent for grid-template-columns: subgrid to take effect, which is why subgrid lives on <GridItem> (the element actually placed as a grid item) rather than on a <Grid> nested inside it.