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.

Wide enough for two columns; narrow the window and it becomes one

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.

No new class needed for a different rail

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.

Visual order reversed; reading order unchanged

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.

A string that cannot wrap, in a column that is allowed to shrink

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.

The heading in the rail is sized by the rail, not by the window

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.

Classes, generated from the stylesheet source
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

Design tokens this component reads
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-start draws it on the left.
  • .split-rail-start separates 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 .drawer rather than a rail.
  • min-inline-size: 0 is 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.

Main on the right, rail on the left, from the same markup

المحتوى الرئيسي

العمود المرن.

<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 .grid if they should collapse.
  • Not for the app frame. A persistent navigation sidebar with a header and a footer is .app-shell plus .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 64rem the 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 .grid with explicit tracks, or reconsider whether the third column is a rail at all.