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.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
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.
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.
Width caps
Three different maximums for three different jobs. Applying one number to all three is what makes a wide monitor uncomfortable.
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.
Every part, every measurement, and the reason it is that number.
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.
- 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.
- Column1fr
Fluid, never fixed. A fixed column width means a fixed breakpoint set, and the layout breaks at every size you did not test.
- 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.
- Container max76rem
In rem, so it scales with the root font size. Past about 1216px, extra width adds eye travel without adding information.
- Prose max68ch
A separate cap, in ch, applied to text blocks inside a wide container. Layout width and reading width are different problems.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Spacing
| Token | Value | Used for |
|---|---|---|
| --ds-layout-gutter | Page padding below 640px | |
| --ds-layout-gutter-lg | Page padding at 640px and above | |
| gap-4 | Default grid gap | |
| gap-6 | Grid gap on wide layouts | |
| --ds-layout-container | Maximum content width. The App Bar reads this token rather than carrying its own cap | |
| sidebar | Resizable navigation rail | |
| topbar | Application header height | |
| toc-rail | On-this-page rail, hidden below 1280px |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Padding | Label gap | Max width | When to use |
|---|---|---|---|---|
| Mobile | 24px | 16px | <640px | 4 columns. Single column content, stacked cards, bottom navigation. |
| Tablet | 32px | 16px | 640–1023px | 8 columns. Two-up cards, collapsible sidebar. |
| Laptop | 40px | 24px | 1024–1279px | 12 columns. Persistent sidebar, three-up cards. |
| Desktop | 40px | 24px | 1280–1535px | 12 columns plus the right rail. |
| Wide | 40px | 24px | ≥1536px | Container caps at 76rem and centres. Do not keep stretching. |
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));@container (min-width: 24rem) { … }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.
Not a checklist to run at the end. These are the requirements the component was built from.
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
| Tab | Follows DOM order, not visual order. CSS Grid can reorder visually — if it does, the keyboard path no longer matches what is on screen. |
| Skip link | The 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.
| Attribute | Applied to | Notes |
|---|---|---|
| <main>, <nav>, <aside> | Grid regions | Landmarks let screen-reader users jump between regions the same way sighted users jump with their eyes. |
| aria-label | Multiple <nav> elements | A page with a sidebar and breadcrumbs has two navs; each needs a distinguishing label. |
| order / grid-area | Any reordering | Visual reordering that does not match DOM order is a WCAG 1.3.2 failure. Change the DOM instead. |
Example usage
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
: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; }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.