Navigation

v0.0.4
Application pages

Master-detail

A list beside what is chosen in it, each scrolling by itself.

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>
At Tablet and Phone the detail leaves and the list takes the width; each row then links to the detail as a page of its own.

#How it is built

  • container-inline on 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-auto on 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-full until the frame reaches 48rem, then w(300)@container(--3xl) with shrink(0). The detail is grow(1) basis(0) min-w(0): it takes the rest and never pushes the list.
  • Below 48rem there is one pane. The detail is hidden and returns with flex@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>
Drag the line between the panes, or focus it and press the arrow keys, Home or End.

#Three ways to build it

WhenUse
The panes are a region of a pageUtilities and a container, as above
The reader resizes the panesA splitter
The panes are the whole appA 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.