Skip to content

Grid & Layout

A 12-column fluid grid, a 16–24px gutter, and hard caps on how wide content is allowed to get. Layout is the frame; everything else hangs off it.

Also called Container, Stack, Flex — in this system all of them are Grid & Layout.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

12-column grid

12 columns · 16px gutter

Application shell

Three fixed decisions in one declaration. The sidebar is resizable but bounded; the rail disappears below 1280px; the content is the only fluid track.

Sidebar

fixed 200–400px

Top bar · 56px

Content

fluid, max-inline-size 76rem

Rail

hidden < 1280px

Self-arranging collections

auto-fill with minmax reflows without a single media query. Resize the stage to watch the column count change on its own.

Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8

Width caps

Three different maximums for three different jobs. Applying one number to all three is what makes a wide monitor uncomfortable.

max-w-[68ch] — Prose. Roughly 11 words per line.

max-w-3xl — Forms and settings. Wide enough for two columns.

max-w-[76rem] — Dashboards and tables. The outer container.

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

12 col≥1024px
8 col640–1023px
4 col<640px
Halves
Thirds
Sidebar200px 1fr
Asymmetric2fr 1fr
Auto-fillminmax(4rem,1fr)

Anatomy

Every part, every measurement, and the reason it is that number.

gutter 24pxcolumn 1frgap 16px

Gutter is the space outside the grid; gap is the space between its columns. Confusing the two is why content sometimes touches the viewport edge on mobile.

  1. Gutter24px mobile · 40px desktop

    The page’s outer padding. 16px is too tight on a modern phone and 32px wastes a fifth of a 360px screen; 24px is the value that survives real content.

  2. Column1fr

    Fluid, never fixed. A fixed column width means a fixed breakpoint set, and the layout breaks at every size you did not test.

  3. Gap16px · 24px on wide

    Must be smaller than the gutter, or the outer columns look detached from the page and the grid stops reading as a unit.

  4. Container max76rem

    In rem, so it scales with the root font size. Past about 1216px, extra width adds eye travel without adding information.

  5. Prose max68ch

    A separate cap, in ch, applied to text blocks inside a wide container. Layout width and reading width are different problems.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Spacing

TokenValueUsed for
--ds-layout-gutterPage padding below 640px
--ds-layout-gutter-lgPage padding at 640px and above
gap-4Default grid gap
gap-6Grid gap on wide layouts
--ds-layout-containerMaximum content width. The App Bar reads this token rather than carrying its own cap
sidebarResizable navigation rail
topbarApplication header height
toc-railOn-this-page rail, hidden below 1280px

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizePaddingLabel gapMax widthWhen to use
Mobile24px16px<640px4 columns. Single column content, stacked cards, bottom navigation.
Tablet32px16px640–1023px8 columns. Two-up cards, collapsible sidebar.
Laptop40px24px1024–1279px12 columns. Persistent sidebar, three-up cards.
Desktop40px24px1280–1535px12 columns plus the right rail.
Wide40px24px≥1536pxContainer caps at 76rem and centres. Do not keep stretching.

Do

Cap the container and centre itOn a 2560px monitor an uncapped layout puts the navigation and the primary action a foot apart. Capping is not wasted space; it is a reading distance.
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
Let collections reflow with auto-fillOne declaration replaces four media queries and stays correct at sizes you never tested — including inside a resized panel, where media queries do not apply at all.
@container (min-width: 24rem) { … }
Use container queries for reusable componentsA card in a 300px sidebar and the same card in a 900px main column need different layouts. Media queries only know the viewport; container queries know the space the component is actually in.
padding-inline: 1.5rempadding-left: 1.5rem
Use logical propertiespadding-inline and margin-block mirror automatically in RTL locales. padding-left does not, and you find out when someone opens the Arabic build.

Don't

Do not nest grids more than two deepEvery level makes the final position of an element harder to predict and harder to debug. Past two, use flex inside the cell.
This sentence is longer than the box that was designed to hold it, so it is now clipped.
Do not set fixed heights on content containersIt fails the WCAG text-spacing override, it fails localisation, and it fails the moment a string is longer than the one you designed with.
left: 42px; top: 18px
Do not use absolute positioning for layoutAbsolutely positioned elements are removed from flow, so nothing around them can respond. It works at exactly one viewport size and breaks at every other.

A line this long forces a return sweep across the entire width of the screen, and after two or three of them most readers give up and start skimming instead, which defeats the purpose of writing the paragraph in the first place.

Do not let prose fill a wide containerA 1200px-wide paragraph is roughly 190 characters per line. The eye loses its place on almost every return sweep.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.3.2Meaningful SequenceA1.4.10ReflowAA1.3.4OrientationAA

Contrast

  • Layout does not carry contrast requirements, but any visible boundary between regions must reach 3:1 if it is the only thing separating them.

Keyboard

TabFollows DOM order, not visual order. CSS Grid can reorder visually — if it does, the keyboard path no longer matches what is on screen.
Skip linkThe first focusable element must jump past the navigation to the main content.

Screen readers

  • Landmarks are the primary navigation mechanism for screen-reader users. A page built entirely from divs offers no way to skip anything.
  • display: contents on a grid item removes it from the accessibility tree in some browser and AT combinations. Avoid it on anything semantic.

Focus & touch

  • Focus must remain visible inside scrollable grid areas. A sticky header can cover a focused element — scroll-padding-top on the scroll container fixes it.
  • Content must reflow at 320px CSS width with no horizontal scrolling. Test at 320 × 256, which is 1280 × 1024 at 400% zoom — the actual WCAG requirement.
AttributeApplied toNotes
<main>, <nav>, <aside>Grid regionsLandmarks let screen-reader users jump between regions the same way sighted users jump with their eyes.
aria-labelMultiple <nav> elementsA page with a sidebar and breadcrumbs has two navs; each needs a distinguishing label.
order / grid-areaAny reorderingVisual reordering that does not match DOM order is a WCAG 1.3.2 failure. Change the DOM instead.

Code

Example usage

tsx
1// Application shell: one grid declares every region2<div className="grid h-dvh" style={{ gridTemplateColumns: 'var(--sidebar) 1fr' }}>3  <Sidebar />4  <div className="grid grid-rows-[56px_1fr] min-w-0">5    <TopBar />6    <main className="overflow-y-auto">{children}</main>7  </div>8</div>910// Page container: capped and centred11<div className="mx-auto w-full max-w-[76rem] px-6 sm:px-10">{children}</div>1213// Prose gets its own, narrower cap14<article className="max-w-[68ch]">{body}</article>1516// Collections reflow with no media queries17<div className="grid gap-4 [grid-template-columns:repeat(auto-fill,minmax(16rem,1fr))]">18  {items.map((i) => <Card key={i.id} {...i} />)}19</div>2021// min-w-0 is mandatory on any grid child that contains text.22// Without it the child's min-content width prevents the track23// from shrinking and the whole layout overflows.24<div className="grid grid-cols-[200px_1fr]">25  <aside />26  <main className="min-w-0">{longContent}</main>27</div>

CSS

css
:root {
  --gutter: 1.5rem;          /* 24px */
  --container: 76rem;
  --sidebar: 268px;
  --topbar: 56px;
}
@media (min-width: 640px) {
  :root { --gutter: 2.5rem; }   /* 40px */
}

.page {
  inline-size: 100%;
  max-inline-size: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The shell. One declaration, no negotiation between children. */
.shell {
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  block-size: 100dvh;
}

/* Collections that arrange themselves */
.collection {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}

/* Container queries: the component adapts to its slot, not the viewport */
.card-host { container-type: inline-size; }
@container (min-width: 24rem) {
  .card { grid-template-columns: 6rem 1fr; }
}

/* dvh, not vh — vh ignores the mobile browser chrome and overflows */
.full-height { block-size: 100dvh; }

Notes

Professional tips

  • min-w-0 on grid and flex children that contain text. The default min-width: auto prevents shrinking below content size and is responsible for most mystery overflows.
  • Use dvh instead of vh for full-height layouts. vh does not account for the collapsing browser chrome on mobile, so the page is always slightly too tall.
  • Prefer subgrid for aligning content across sibling cards. It is the only way to make three cards with different-length titles align their footers.
  • Design the 320px and the 1920px cases first. Everything in between is interpolation; the extremes are where layouts actually break.

Performance

  • CSS Grid layout is fast, but a grid with thousands of implicit rows is not. Virtualise long lists rather than letting the grid create 10,000 tracks.
  • Container queries require container-type, which creates a containment context and a new layout boundary. That is usually a performance win, but it disables percentage-based heights inside.
  • Avoid layouts that depend on JavaScript measurement. Every measure-then-style cycle is a forced synchronous layout, and they compound during resize.
  • content-visibility: auto on long sections skips layout and paint for offscreen content — often the single biggest win on a long documentation page.

Common mistakes

  • Forgetting min-w-0, so a long unbroken string makes a grid track overflow its container and produces a horizontal scrollbar on the whole page.
  • Using vh for the app shell, so on iOS the bottom of the layout sits under the browser toolbar.
  • Reordering with CSS so the visual order and the tab order disagree. It is a real accessibility failure, not a nitpick.
  • Setting overflow: hidden on a grid parent to hide an overflow bug instead of fixing the track sizing. The content is still there, just unreachable.

Real-world recommendations

  • Resize the browser slowly from 320px to 2560px on every new page. Most layout bugs live in the ranges nobody designs for, between the breakpoints.
  • A resizable sidebar needs min and max bounds and a double-click reset. Unbounded resize always ends with someone dragging it to 4px and being unable to find it again.
  • For dashboards, prefer a fixed set of layout templates over a free-form drag-and-drop grid. Users almost never rearrange, and the templates stay consistent and testable.
  • When a layout needs "just one more breakpoint", the component usually wants a container query instead. Viewport breakpoints multiply; container queries do not.