Split
.split is content plus a rail. One column below
64rem, two above it: a flexible main column and a fixed-width sidebar
whose size comes from --rail, defaulting to
20rem. It is a grid, not a flex row, because a grid can give one track
a fixed width without either child having to know about the other.
When to use it
Use it for a page or a panel that has a primary column and a secondary one: an article with a table of contents, a form with a summary, a dashboard with filters. The rail goes second in the markup, which is also the order it should be read in.
Invoice INV-2291
Two licences, billed annually. Charged to the card ending 4242.
The main column takes whatever is left.
<div class="split">
<div class="stack stack-3">
<h3>Invoice INV-2291</h3>
<p>Two licences, billed annually. Charged to the card ending 4242.</p>
<p class="text-muted">The main column takes whatever is left.</p>
</div>
<aside class="panel stack stack-2">
<strong>Summary</strong>
<p class="text-muted">The rail is 20rem, and only exists above 64rem.</p>
</aside>
</div>
<div class="split">
<div class="stack stack-3">
<h3>Invoice INV-2291</h3>
<p>Two licences, billed annually. Charged to the card ending 4242.</p>
<p class="text-muted">The main column takes whatever is left.</p>
</div>
<aside class="panel stack stack-2">
<strong>Summary</strong>
<p class="text-muted">The rail is 20rem, and only exists above 64rem.</p>
</aside>
</div>
The rail width
--rail sets the second track. It takes a length rather than a fraction,
which is the point: a sidebar with a fixed width is predictable, and the main column
absorbs the difference as the window changes.
A narrower rail
--rail:12rem, set inline.
<div class="split" style="--rail:12rem">
<div class="stack stack-2">
<h3>A narrower rail</h3>
<p class="text-muted">--rail:12rem, set inline.</p>
</div>
<aside class="panel"><p class="text-muted">12rem</p></aside>
</div>
<div class="split" style="--rail:12rem">
<div class="stack stack-2">
<h3>A narrower rail</h3>
<p class="text-muted">--rail:12rem, set inline.</p>
</div>
<aside class="panel"><p class="text-muted">12rem</p></aside>
</div>
The main track is minmax(0, 1fr) rather than 1fr. That
zero minimum is what lets the main column shrink below its content's natural width,
and it is the same defence as the min-inline-size rule below.
Rail on the starting edge
.split-rail-start swaps the track order so the rail comes first
visually. The markup order does not change — the rail is still the
second child — so this is a case where visual order and DOM order deliberately
differ. That is defensible for a navigation rail that should be read after the
content but appear before it; it is not defensible if the rail contains the first
thing a reader needs.
Main content, second visually
Still first in the markup, so still first for a screen reader.
<div class="split split-rail-start">
<div class="stack stack-2">
<h3>Main content, second visually</h3>
<p class="text-muted">Still first in the markup, so still first for a screen reader.</p>
</div>
<aside class="panel"><p class="text-muted">Rail, on the starting edge</p></aside>
</div>
<div class="split split-rail-start">
<div class="stack stack-2">
<h3>Main content, second visually</h3>
<p class="text-muted">Still first in the markup, so still first for a screen reader.</p>
</div>
<aside class="panel"><p class="text-muted">Rail, on the starting edge</p></aside>
</div>
Because the rail is placed by grid-template-columns rather than by
order, the tab order still follows the markup: a keyboard user reaches
the main content first and then the rail, even though the rail is on the left. Decide
whether that is what you want before reaching for this class.
The one line that stops a page scrolling sideways
.split > * { min-inline-size: 0; }
A grid item's min-inline-size is auto, which means its
minimum size is its content. A child that cannot shrink — a wide table, a long code
block, a form row, an unbroken URL — therefore makes its track wider than the track
was supposed to be, and the whole grid grows past its container. The page then
scrolls sideways, and the thing that caused it is usually nowhere near the thing that
looks wrong.
This is the single most common CSS grid bug there is, and Deck has hit it twice: once in the docs shell, where a wide table dragged the sidebar off-screen, and once in the demo, where a two-column form overflowed a 375px viewport by exactly 16px until this line existed.
https://example.com/exports/2026/03/a-very-long-generated-filename-that-cannot-wrap.csv
The column shrinks because it is allowed to.
<div class="split" style="--rail:8rem">
<div class="stack stack-2">
<p class="truncate">https://example.com/exports/2026/03/a-very-long-generated-filename-that-cannot-wrap.csv</p>
<p class="text-muted">The column shrinks because it is allowed to.</p>
</div>
<aside class="panel"><p class="text-muted">Rail</p></aside>
</div>
<div class="split" style="--rail:8rem">
<div class="stack stack-2">
<p class="truncate">https://example.com/exports/2026/03/a-very-long-generated-filename-that-cannot-wrap.csv</p>
<p class="text-muted">The column shrinks because it is allowed to.</p>
</div>
<aside class="panel"><p class="text-muted">Rail</p></aside>
</div>
min-inline-size: 0 lets the track shrink; it does not decide what
happens to the content inside it. Pair it with .truncate,
overflow-x: auto, or overflow-wrap: anywhere depending on
whether the content should be clipped, scrolled or broken.
A rail that knows it is narrow
src/18-container.css makes a .cq child of a split its own
query container, so what is inside the rail can respond to the rail's width rather
than the viewport's. That matters here more than anywhere else in Deck: a component
in a 20rem rail on a 1600px monitor is in a narrow space
while every viewport media query says the page is wide.
Main
Plenty of room here.
<div class="split" style="--rail:16rem">
<div class="stack stack-2"><h3>Main</h3><p class="text-muted">Plenty of room here.</p></div>
<aside class="cq">
<div class="card">
<div class="card-body"><h3 class="card-title text-cq">Sized by the rail</h3></div>
</div>
</aside>
</div>
<div class="split" style="--rail:16rem">
<div class="stack stack-2"><h3>Main</h3><p class="text-muted">Plenty of room here.</p></div>
<aside class="cq">
<div class="card">
<div class="card-body"><h3 class="card-title text-cq">Sized by the rail</h3></div>
</div>
</aside>
</div>
The one breakpoint
.split contains a @media (min-width: 64rem), which makes it
one of the few things in Deck that has a breakpoint at all. It is here because the
decision — is there room for a sidebar beside the content — genuinely depends on how
wide the page is, and because a split is nearly always the outermost layout
on a page.
If your split is not the outermost layout — a split inside a modal, a drawer or
another rail — the viewport is the wrong thing to ask, and the media query will give
you two columns in a space that has room for one. Use a container query of your own
for that case; .field-row-cq is the same
problem solved for form fields.
Gap
Same --gap, but with a looser default than the other primitives:
1.5rem, because two columns of
content need more separation than two items in a row.
Classes
Generated from src/04-layout.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.split |
deck.layout |
Content + rail. Collapses to one column below the width it needs. | 04-layout.css:129 |
.split-rail-start |
deck.layout |
Puts the rail on the starting edge without touching markup order, so the main column stays first for a screen reader and for the keyboard. Use it for supplementary navigation; not for a rail holding the first thing a reader needs. | 04-layout.css:146 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--space-4 |
1rem |
|
--space-6 |
1.5rem |
|
--space-8 |
2rem |
--rail is a local property with a 20rem fallback rather
than a global token, because a rail width is a per-layout decision and not part of
the design system.
Accessibility
-
Markup order is reading order and tab order. Put the main content
first. Everything on this page follows from that: the rail is the second child even
when
.split-rail-startdraws it on the left. -
.split-rail-startseparates visual and DOM order on purpose, which is the one thing this page asks you to think about. It is fine for supplementary navigation; it is wrong if a sighted reader would encounter something in the rail before the content and a keyboard user would not. -
The rail should usually be an
<aside>, which is a complementary landmark a screen reader can jump to. A bare<div>is fine when the rail is decoration rather than a distinct region. -
Below 64rem the rail follows the content. On a phone the sidebar
is at the bottom of the page. That is correct for a summary and wrong for filters
the reader needs before they scroll — in which case put the filters in a
.drawerrather than a rail. -
min-inline-size: 0is an accessibility fix as much as a layout one. Horizontal scrolling at 320px or 400% zoom fails WCAG's reflow criterion, and an overflowing grid track is the usual cause. - Two columns can double the tab distance. A reader tabbing through the main column reaches the rail only after everything in the content. If the rail holds a frequently used control, that is a long way; a skip link or putting the control in the content is the answer.
Right to left
Grid columns follow the inline direction, so under dir="rtl" the main
column is on the right and the rail on the left, with no extra rules.
.split-rail-start means the starting edge, not the left one, so
it also mirrors — which is exactly why it is not called
.split-rail-left.
المحتوى الرئيسي
العمود المرن.
<div dir="rtl" class="split" style="--rail:12rem">
<div class="stack stack-2"><h3>المحتوى الرئيسي</h3><p class="text-muted">العمود المرن.</p></div>
<aside class="panel"><p class="text-muted">الشريط الجانبي</p></aside>
</div>
<div dir="rtl" class="split" style="--rail:12rem">
<div class="stack stack-2"><h3>المحتوى الرئيسي</h3><p class="text-muted">العمود المرن.</p></div>
<aside class="panel"><p class="text-muted">الشريط الجانبي</p></aside>
</div>
Reduced motion
Nothing animates. Collapsing to one column at 64rem happens at layout
time and is not a transition.
Printing
src/99-print.css sets .split { display: block }. Two
columns on paper means a reader finishing column one at the bottom of page four and
going back to page one for column two, and a grid track cannot break across pages
anyway. The rail therefore prints after the content, in markup order — which is
another reason for the main content to be first.
Overriding it
/* One split, no layer needed */
<div class="split" style="--rail:16rem;--gap:var(--space-8)">
@layer app.components {
/* Split sooner than 64rem */
@media (min-width: 52rem) {
.split { grid-template-columns: minmax(0, 1fr) var(--rail, 20rem); }
}
/* A rail that keeps a share of the width rather than a fixed size */
.split-fluid { --rail: minmax(14rem, 24rem); }
}
--rail is dropped into grid-template-columns verbatim, so
it accepts any track size — minmax(), fit-content(), a
percentage — not only a length.
When not to use it
-
Not for two equal columns. A split has one flexible column and one
fixed rail. Two things of equal weight are
.grid-fixed-2, or.gridif they should collapse. -
Not for the app frame. A persistent navigation sidebar with a
header and a footer is
.app-shellplus.sidebar, which handle the full height and the scroll behaviour. A split is a content layout. - Not inside a modal or a drawer. Its breakpoint asks about the viewport, and inside a narrow container the viewport is the wrong question. Use a container query.
-
Not for a rail the reader needs first on a phone. Below
64remthe rail is at the bottom. Filters, a summary the reader acts on, or a call to action belong in the content column or in a.drawer. -
Not three columns. Nesting a split inside a split gives you two
breakpoints fighting over the same viewport. Use
.gridwith explicit tracks, or reconsider whether the third column is a rail at all.