Master-detail sets a list beside whatever is chosen in it: keys and a key, messages and a message, files and a file. Each pane scrolls by itself, so the list keeps its place while the reader moves through the detail.
This one is written with utilities alone. It is small enough to live inside a page, and it answers to its own width with container queries, so it works in a wide page and in a narrow panel alike.
Master-detail
<section class="sheet container-inline overflow-hidden text-sm fg-text-body" aria-labelledby="keys-title"> <div class="flex h(440)"> <div class="flex flex-col w-full w(300)@container(--3xl) shrink(0) bxe(1)@container(--3xl) bxe-solid@container(--3xl) b-line-hair"> <header class="flex flex-wrap items-center justify-between gap-sm p-md bb(1) bb-solid b-line-hair"> <h2 class="heading heading-sm" id="keys-title">API keys</h2> <button type="button" class="btn btn-sm"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M12 5v14M5 12h14" /> </svg>New key</button> </header> <ul class="list list-interactive grow(1) overflow-y-auto"> <li> <a class="list-row" href="#" aria-current="true"> <div class="list-body"> <p class="list-title">Production</p> <p class="list-description">Created 2 Mar · last used today</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Staging</p> <p class="list-description">Created 14 Jan · last used yesterday</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">CI</p> <p class="list-description">Created 9 Dec · last used 3 days ago</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Local, Ada</p> <p class="list-description">Created 1 Nov · last used 2 weeks ago</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Local, Alan</p> <p class="list-description">Created 28 Oct · never used</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Legacy importer</p> <p class="list-description">Created 4 Jun · last used in July</p> </div> </a> </li> </ul> </div> <article class="hidden flex@container(--3xl) flex-col gap-lg grow(1) basis(0) min-w(0) overflow-y-auto p-lg"> <header class="flex flex-wrap items-start justify-between gap-sm"> <div class="flex flex-col gap-3xs"> <h3 class="heading">Production</h3> <p class="m(0) fg-text-muted">Used by the deploy workflow on the main branch.</p> </div> <span class="badge badge-success badge-dot">Active</span> </header> <dl class="data-list"> <div class="data-list-item"> <dt class="data-list-term">Key</dt> <dd class="data-list-value"><code class="code">atl_live_••••••••4f2a</code> </dd> </div> <div class="data-list-item"> <dt class="data-list-term">Scope</dt> <dd class="data-list-value">Deploy, read logs</dd> </div> <div class="data-list-item"> <dt class="data-list-term">Created</dt> <dd class="data-list-value">2 March by Ada Lovelace</dd> </div> <div class="data-list-item"> <dt class="data-list-term">Last used</dt> <dd class="data-list-value">Today at 09:41, from Tokyo</dd> </div> </dl> <div class="flex gap-xs mt-auto"> <button type="button" class="btn btn-sm">Rotate key</button> <button type="button" class="btn btn-sm btn-danger">Revoke</button> </div> </article> </div></section>#How it is built
container-inlineon the frame makes it a size container. Every@container(…)utility inside measures the frame, not the screen.- A row with a fixed height.
flex h(440)gives both panes the same height;overflow-y-autoon each makes them scroll alone. In a full-height app, take the height from a fixed shell instead of a number. - The list is fixed and the detail is fluid. The list is
w-fulluntil the frame reaches 48rem, thenw(300)@container(--3xl)withshrink(0). The detail isgrow(1) basis(0) min-w(0): it takes the rest and never pushes the list. - Below 48rem there is one pane. The detail is
hiddenand returns withflex@container(--3xl). A narrow screen shows the list; choosing a row goes to the detail on its own page. That is navigation, so it belongs to your router, not to the layout. - The line between the panes is
bxe(1)@container(--3xl) bxe-solid@container(--3xl): it exists only while there are two panes.
The current row is marked with aria-current="true" on a list-row in a list-interactive.
#Resizable
When the reader should decide how the panes share the width, use a splitter: the same two panes with a handle between them. The splitter is a component, because its handle has states and a keyboard contract; the splitter behavior moves it, and its page lists the keys.
Resizable
<section class="splitter sheet overflow-hidden h(440) text-sm fg-text-body" style="--splitter-size: 36%" aria-labelledby="keys-split-title"> <div class="splitter-pane flex flex-col"> <header class="flex flex-wrap items-center justify-between gap-sm p-md bb(1) bb-solid b-line-hair"> <h2 class="heading heading-sm" id="keys-split-title">API keys</h2> <button type="button" class="btn btn-sm"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M12 5v14M5 12h14" /> </svg>New key</button> </header> <ul class="list list-interactive"> <li> <a class="list-row" href="#" aria-current="true"> <div class="list-body"> <p class="list-title">Production</p> <p class="list-description">Created 2 Mar · last used today</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Staging</p> <p class="list-description">Created 14 Jan · last used yesterday</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">CI</p> <p class="list-description">Created 9 Dec · last used 3 days ago</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Local, Ada</p> <p class="list-description">Created 1 Nov · last used 2 weeks ago</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Local, Alan</p> <p class="list-description">Created 28 Oct · never used</p> </div> </a> </li> <li> <a class="list-row" href="#"> <div class="list-body"> <p class="list-title">Legacy importer</p> <p class="list-description">Created 4 Jun · last used in July</p> </div> </a> </li> </ul> </div> <div class="splitter-handle" role="separator" tabindex="0" aria-label="Resize the list" aria-valuenow="36" aria-valuemin="25" aria-valuemax="60"></div> <article class="splitter-pane flex flex-col gap-lg p-lg"> <header class="flex flex-wrap items-start justify-between gap-sm"> <div class="flex flex-col gap-3xs"> <h3 class="heading">Production</h3> <p class="m(0) fg-text-muted">Used by the deploy workflow on the main branch.</p> </div> <span class="badge badge-success badge-dot">Active</span> </header> <dl class="data-list"> <div class="data-list-item"> <dt class="data-list-term">Key</dt> <dd class="data-list-value"><code class="code">atl_live_••••••••4f2a</code> </dd> </div> <div class="data-list-item"> <dt class="data-list-term">Scope</dt> <dd class="data-list-value">Deploy, read logs</dd> </div> <div class="data-list-item"> <dt class="data-list-term">Created</dt> <dd class="data-list-value">2 March by Ada Lovelace</dd> </div> <div class="data-list-item"> <dt class="data-list-term">Last used</dt> <dd class="data-list-value">Today at 09:41, from Tokyo</dd> </div> </dl> <div class="flex gap-xs mt-auto"> <button type="button" class="btn btn-sm">Rotate key</button> <button type="button" class="btn btn-sm btn-danger">Revoke</button> </div> </article></section><script type="module"> import { attach } from '@master/ui/behaviors' attach()</script>#Three ways to build it
| When | Use |
|---|---|
| The panes are a region of a page | Utilities and a container, as above |
| The reader resizes the panes | A splitter |
| The panes are the whole app | A fixed shell, with the list as its sidebar |
#Accessibility
- The list is a list of links, each to a detail.
aria-current="true"marks the one on show. - Label the frame, and give the detail a heading that names what was chosen, so a reader who lands in it knows where they are.
- A pane that scrolls and holds nothing focusable needs
tabindex="0", so the keyboard can scroll it. - When the detail is hidden on a narrow screen it is
display: none, out of the accessibility tree; do not leave a control that points at it.
#Related
- ListRows of things with media, words and something at the end.
- Data listThe facts about one thing, as terms and values.
- SplitterTwo panes and the handle between them that sets how they share the space.
- SurfaceTrays, sheets and wells: the material ladder every component is built on.
- BadgeA small chip for a status, a category or a count.