Navigation

v0.0.4
Application pages

App shell

A sidebar that stays while the main region scrolls, and a drawer when it is narrow.

An app shell is the frame of an application: a sidebar that stays in place while the main region moves. It is built on the shell layout classes, which hold the parts utilities cannot write: heights in dvh, the sticky sidebar with its own scroll, the inset for the home indicator, and the width at which the sidebar steps aside.

The shell draws nothing. In the example the hairline is a border utility, the bar is a navbar, the links are a nav and the tiles are cards in an auto grid.

App shell

<div class="shell bg-surface-base fg-text-body text-sm">  <a class="skip" href="#app-main">Skip to content</a>  <aside class="shell-sidebar gap-md p-sm bxe(1) bxe-solid b-line-hair">    <a class="navbar-brand h(40) px-xs" href="#">      <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>Atlas</a>    <nav class="nav" aria-label="Workspace">      <a class="nav-item" href="#">        <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="M5 12 12 5l7 7M7 10v9h10v-9" />        </svg>Overview</a>      <a class="nav-item" href="#" aria-current="page">        <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />        </svg>Projects        <span class="nav-end">12</span>      </a>      <a class="nav-item" href="#">        <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />        </svg>Domains</a>      <a class="nav-item" href="#">        <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />        </svg>Analytics</a>      <a class="nav-item" href="#">        <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />        </svg>Team</a>      <a class="nav-item" href="#">        <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />        </svg>Settings</a>    </nav>    <button type="button" class="btn btn-quiet justify-start mt-auto">      <span class="avatar avatar-xs">        <img class="avatar-image" src="/demo/portrait-1.svg" alt>      </span>Ada Lovelace</button>  </aside>  <main class="shell-main" id="app-main" tabindex="-1">    <header class="navbar">      <button type="button" class="btn btn-quiet btn-square btn-sm shell-toggle" commandfor="shell-nav" command="show-modal" aria-label="Open navigation">        <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="M4 8h16M4 16h16" />        </svg>      </button>      <h1 class="heading heading-sm">Projects</h1>      <div class="navbar-end">        <button type="button" class="btn btn-quiet btn-square btn-sm" aria-label="Search">          <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="M4 11a7 7 0 1 0 14 0a7 7 0 0 0-14 0M20 20l-4-4" />          </svg>        </button>        <button type="button" class="btn btn-primary 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 project</button>      </div>    </header>    <div class="flex flex-col gap-lg p-lg">      <div class="auto-grid">        <article class="card">          <header class="card-header">            <h3 class="card-title">Atlas</h3>            <p class="card-description">atlas.dev</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card">          <header class="card-header">            <h3 class="card-title">Borealis</h3>            <p class="card-description">borealis.app</p>            <span class="badge badge-info badge-dot card-action">Building</span>          </header>        </article>        <article class="card">          <header class="card-header">            <h3 class="card-title">Cinder</h3>            <p class="card-description">cinder.io</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card">          <header class="card-header">            <h3 class="card-title">Drift</h3>            <p class="card-description">drift.so</p>            <span class="badge badge-danger badge-dot card-action">Failed</span>          </header>        </article>        <article class="card">          <header class="card-header">            <h3 class="card-title">Ember</h3>            <p class="card-description">ember.dev</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card">          <header class="card-header">            <h3 class="card-title">Fathom</h3>            <p class="card-description">fathom.app</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>      </div>    </div>  </main>  <dialog class="dialog dialog-drawer dialog-drawer-start" id="shell-nav" closedby="any" aria-label="Navigation">    <div class="dialog-body flex flex-col gap-md">      <a class="navbar-brand h(40) px-xs" href="#">        <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />        </svg>Atlas</a>      <nav class="nav" aria-label="Workspace, in the drawer">        <a class="nav-item" href="#">          <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="M5 12 12 5l7 7M7 10v9h10v-9" />          </svg>Overview</a>        <a class="nav-item" href="#" aria-current="page">          <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />          </svg>Projects          <span class="nav-end">12</span>        </a>        <a class="nav-item" href="#">          <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />          </svg>Domains</a>        <a class="nav-item" href="#">          <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />          </svg>Analytics</a>        <a class="nav-item" href="#">          <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />          </svg>Team</a>        <a class="nav-item" href="#">          <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />          </svg>Settings</a>      </nav>    </div>    <button type="button" class="btn btn-quiet btn-square btn-sm dialog-close" commandfor="shell-nav" command="close" aria-label="Close navigation">      <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="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </dialog></div>
Scroll the page: the sidebar and the bar stay. Choose Phone: the sidebar leaves and a button that opens it as a drawer appears.

#How it is built

PartClassWhat it does
The frameshellA grid at least as tall as the viewport, and a size container named shell
The sidebarshell-sidebarSticks under the header, as tall as the room left in the viewport, with its own scroll
The main regionshell-mainTakes the column that is left and never grows past it
The toggleshell-toggleAny control that should exist only while the sidebar is away

By default the document scrolls. A sticky navbar keeps working, links to headings land where they should, and the browser restores the scroll position on the way back.

Below 48rem of shell the sidebar is removed from the layout. Nothing replaces it by itself: put the same navigation in a drawer on the start edge and open it from a button with shell-toggle. The drawer is a native <dialog>, opened by commandfor, so the whole arrangement needs no script.

The threshold measures the shell, not the screen: a shell inside a narrow panel collapses there, on any display.

#On a tray

Set the shell on the tray tone and stand the main region on a sheet, 8px in. The sidebar then reads as part of the frame and the content as the thing on it. The same classes; only the surfaces changed.

On a tray

<div class="shell bg-surface-tray fg-text-body text-sm">  <a class="skip" href="#app-main-inset">Skip to content</a>  <aside class="shell-sidebar gap-md p-sm">    <a class="navbar-brand h(40) px-xs" href="#">      <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>Atlas</a>    <nav class="nav" aria-label="Workspace">      <a class="nav-item" href="#">        <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="M5 12 12 5l7 7M7 10v9h10v-9" />        </svg>Overview</a>      <a class="nav-item" href="#" aria-current="page">        <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />        </svg>Projects        <span class="nav-end">12</span>      </a>      <a class="nav-item" href="#">        <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />        </svg>Domains</a>      <a class="nav-item" href="#">        <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />        </svg>Analytics</a>      <a class="nav-item" href="#">        <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />        </svg>Team</a>      <a class="nav-item" href="#">        <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />        </svg>Settings</a>    </nav>    <button type="button" class="btn btn-quiet justify-start mt-auto">      <span class="avatar avatar-xs">        <img class="avatar-image" src="/demo/portrait-1.svg" alt>      </span>Ada Lovelace</button>  </aside>  <div class="shell-main flex flex-col p(8) pxs(0)@container(--3xl)">    <main class="sheet grow(1)" id="app-main-inset" tabindex="-1">      <header class="navbar" style="--navbar-color: var(--color-surface-raised)">        <button type="button" class="btn btn-quiet btn-square btn-sm shell-toggle" commandfor="shell-nav-inset" command="show-modal" aria-label="Open navigation">          <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="M4 8h16M4 16h16" />          </svg>        </button>        <h1 class="heading heading-sm">Projects</h1>        <div class="navbar-end">          <button type="button" class="btn btn-primary 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 project</button>        </div>      </header>      <div class="auto-grid p-lg">        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Atlas</h3>            <p class="card-description">atlas.dev</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Borealis</h3>            <p class="card-description">borealis.app</p>            <span class="badge badge-info badge-dot card-action">Building</span>          </header>        </article>        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Cinder</h3>            <p class="card-description">cinder.io</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Drift</h3>            <p class="card-description">drift.so</p>            <span class="badge badge-danger badge-dot card-action">Failed</span>          </header>        </article>        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Ember</h3>            <p class="card-description">ember.dev</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>        <article class="card card-flat">          <header class="card-header">            <h3 class="card-title">Fathom</h3>            <p class="card-description">fathom.app</p>            <span class="badge badge-success badge-dot card-action">Ready</span>          </header>        </article>      </div>    </main>  </div>  <dialog class="dialog dialog-drawer dialog-drawer-start" id="shell-nav-inset" closedby="any" aria-label="Navigation">    <div class="dialog-body flex flex-col gap-md">      <a class="navbar-brand h(40) px-xs" href="#">        <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />        </svg>Atlas</a>      <nav class="nav" aria-label="Workspace, in the drawer">        <a class="nav-item" href="#">          <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="M5 12 12 5l7 7M7 10v9h10v-9" />          </svg>Overview</a>        <a class="nav-item" href="#" aria-current="page">          <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />          </svg>Projects          <span class="nav-end">12</span>        </a>        <a class="nav-item" href="#">          <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />          </svg>Domains</a>        <a class="nav-item" href="#">          <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />          </svg>Analytics</a>        <a class="nav-item" href="#">          <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />          </svg>Team</a>        <a class="nav-item" href="#">          <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />          </svg>Settings</a>      </nav>    </div>    <button type="button" class="btn btn-quiet btn-square btn-sm dialog-close" commandfor="shell-nav-inset" command="close" aria-label="Close navigation">      <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="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </dialog></div>

The navbar's pane is tinted with the surface behind it. On a sheet, set --navbar-color to the sheet's color.

#With a header

shell-header spans the shell above both columns. The sidebar then sticks under it: the shell reserves --shell-header, 3.5rem by default, which is a navbar's height. Change the property when your header is taller.

With a header

<div class="shell bg-surface-base fg-text-body text-sm">  <a class="skip" href="#app-main-header">Skip to content</a>  <header class="navbar shell-header bb(1) bb-solid b-line-hair">    <button type="button" class="btn btn-quiet btn-square btn-sm shell-toggle" commandfor="shell-nav-header" command="show-modal" aria-label="Open navigation">      <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="M4 8h16M4 16h16" />      </svg>    </button>    <a class="navbar-brand" href="#">      <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>Atlas</a>    <div class="navbar-end">      <button type="button" class="btn btn-quiet btn-square btn-sm" aria-label="Notifications">        <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="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" />        </svg>      </button>      <span class="avatar avatar-sm">        <img class="avatar-image" src="/demo/portrait-1.svg" alt="Ada Lovelace">      </span>    </div>  </header>  <aside class="shell-sidebar p-sm bxe(1) bxe-solid b-line-hair">    <nav class="nav" aria-label="Workspace">      <a class="nav-item" href="#">        <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="M5 12 12 5l7 7M7 10v9h10v-9" />        </svg>Overview</a>      <a class="nav-item" href="#" aria-current="page">        <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />        </svg>Projects        <span class="nav-end">12</span>      </a>      <a class="nav-item" href="#">        <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />        </svg>Domains</a>      <a class="nav-item" href="#">        <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />        </svg>Analytics</a>      <a class="nav-item" href="#">        <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />        </svg>Team</a>      <a class="nav-item" href="#">        <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />        </svg>Settings</a>    </nav>  </aside>  <main class="shell-main flex flex-col gap-lg p-lg" id="app-main-header" tabindex="-1">    <h1 class="heading heading-lg">Projects</h1>    <div class="auto-grid">      <article class="card">        <header class="card-header">          <h3 class="card-title">Atlas</h3>          <p class="card-description">atlas.dev</p>          <span class="badge badge-success badge-dot card-action">Ready</span>        </header>      </article>      <article class="card">        <header class="card-header">          <h3 class="card-title">Borealis</h3>          <p class="card-description">borealis.app</p>          <span class="badge badge-info badge-dot card-action">Building</span>        </header>      </article>      <article class="card">        <header class="card-header">          <h3 class="card-title">Cinder</h3>          <p class="card-description">cinder.io</p>          <span class="badge badge-success badge-dot card-action">Ready</span>        </header>      </article>      <article class="card">        <header class="card-header">          <h3 class="card-title">Drift</h3>          <p class="card-description">drift.so</p>          <span class="badge badge-danger badge-dot card-action">Failed</span>        </header>      </article>      <article class="card">        <header class="card-header">          <h3 class="card-title">Ember</h3>          <p class="card-description">ember.dev</p>          <span class="badge badge-success badge-dot card-action">Ready</span>        </header>      </article>      <article class="card">        <header class="card-header">          <h3 class="card-title">Fathom</h3>          <p class="card-description">fathom.app</p>          <span class="badge badge-success badge-dot card-action">Ready</span>        </header>      </article>    </div>  </main>  <dialog class="dialog dialog-drawer dialog-drawer-start" id="shell-nav-header" closedby="any" aria-label="Navigation">    <div class="dialog-body flex flex-col gap-md">      <a class="navbar-brand h(40) px-xs" href="#">        <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 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />        </svg>Atlas</a>      <nav class="nav" aria-label="Workspace, in the drawer">        <a class="nav-item" href="#">          <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="M5 12 12 5l7 7M7 10v9h10v-9" />          </svg>Overview</a>        <a class="nav-item" href="#" aria-current="page">          <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="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" />          </svg>Projects          <span class="nav-end">12</span>        </a>        <a class="nav-item" href="#">          <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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />          </svg>Domains</a>        <a class="nav-item" href="#">          <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="M4 19h16M7 16V9M12 16V5M17 16v-4" />          </svg>Analytics</a>        <a class="nav-item" href="#">          <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="M5 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2M16 3.1a4 4 0 0 1 0 7.8M21 21v-2a4 4 0 0 0-3-3.85" />          </svg>Team</a>        <a class="nav-item" href="#">          <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="M14 6a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 6h10M18 6h2M6 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 12h2M10 12h10M15 18a2 2 0 1 0 4 0a2 2 0 0 0-4 0M4 18h11M19 18h1" />          </svg>Settings</a>      </nav>    </div>    <button type="button" class="btn btn-quiet btn-square btn-sm dialog-close" commandfor="shell-nav-header" command="close" aria-label="Close navigation">      <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="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </dialog></div>

#Fixed

shell-fixed makes the shell exactly the viewport. Nothing scrolls but the regions, each by itself: the list keeps its place while the reader moves through what they opened. Use it for an inbox, an editor or a chat, where two things are read side by side. Use the default for everything else: a document that scrolls as a whole behaves better on a phone, where the browser's toolbars collapse as the page moves.

--shell-sidebar sets the sidebar's width; here it is 20rem, for a list of messages.

Fixed

<div class="shell shell-fixed bg-surface-base fg-text-body text-sm" style="--shell-sidebar: 20rem">  <aside class="shell-sidebar bxe(1) bxe-solid b-line-hair">    <header class="flex items-center justify-between p-md">      <h1 class="heading heading-sm">Inbox</h1>      <button type="button" class="btn btn-quiet btn-square btn-sm" aria-label="Filter">        <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="M4 6h16M7 12h10M10 18h4" />        </svg>      </button>    </header>    <ul class="list list-interactive">      <li>        <a class="list-row" href="#" aria-current="true">          <div class="list-body">            <p class="list-title">Deploy failed on drift.so</p>            <p class="list-description">The install step exited with code 1.</p>          </div>          <span class="list-end">09:41</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Ada invited you to Borealis</p>            <p class="list-description">You now have access as an editor.</p>          </div>          <span class="list-end">09:12</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Your invoice for October</p>            <p class="list-description">$48.00, paid with the card ending 4242.</p>          </div>          <span class="list-end">Tue</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Domain atlas.dev renewed</p>            <p class="list-description">Valid until 4 November next year.</p>          </div>          <span class="list-end">Mon</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Weekly summary</p>            <p class="list-description">1.28M requests, 0.02% errors.</p>          </div>          <span class="list-end">Mon</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Cinder is live in 14 regions</p>            <p class="list-description">Deployed from main by Alan.</p>          </div>          <span class="list-end">Sun</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">New sign-in from Tokyo</p>            <p class="list-description">If this was you, there is nothing to do.</p>          </div>          <span class="list-end">Sat</span>        </a>      </li>      <li>        <a class="list-row" href="#">          <div class="list-body">            <p class="list-title">Usage at 80% of your plan</p>            <p class="list-description">Upgrade or lower traffic before the 30th.</p>          </div>          <span class="list-end">Fri</span>        </a>      </li>    </ul>  </aside>  <main class="shell-main">    <button type="button" class="btn btn-sm shell-toggle m-md" commandfor="inbox-list" command="show-modal">Inbox</button>    <article class="flex flex-col gap-md max-w(640) p-xl">      <p class="eyebrow">Deployments</p>      <h2 class="heading heading-xl">Deploy failed on drift.so</h2>      <p class="lead lead-sm">The install step exited with code 1. Nothing was published, and the previous deployment is still serving traffic.</p> <pre class="code-block"><code>npm error code ERESOLVEnpm error ERESOLVE could not resolvenpm error While resolving: drift@2.4.0</code></pre>      <p class="m(0) leading-md">The adapter that drift pins was withdrawn from the registry this morning, so the lockfile no longer resolves. Two other projects in this team pin the same version.</p>      <p class="m(0) leading-md">Retry the deployment after pinning the dependency, or roll back from the deployments list. The log above is the last twenty lines; the full log is on the deployment page.</p>      <div class="flex gap-xs">        <button type="button" class="btn btn-primary">Retry deployment</button>        <button type="button" class="btn">View full log</button>      </div>    </article>  </main>  <dialog class="dialog dialog-drawer dialog-drawer-start" id="inbox-list" closedby="any" aria-label="Inbox">    <div class="dialog-body flex flex-col gap-md">      <h2 class="heading heading-sm">Inbox</h2>      <ul class="list list-interactive">        <li>          <a class="list-row" href="#" aria-current="true">            <div class="list-body">              <p class="list-title">Deploy failed on drift.so</p>              <p class="list-description">The install step exited with code 1.</p>            </div>            <span class="list-end">09:41</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Ada invited you to Borealis</p>              <p class="list-description">You now have access as an editor.</p>            </div>            <span class="list-end">09:12</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Your invoice for October</p>              <p class="list-description">$48.00, paid with the card ending 4242.</p>            </div>            <span class="list-end">Tue</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Domain atlas.dev renewed</p>              <p class="list-description">Valid until 4 November next year.</p>            </div>            <span class="list-end">Mon</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Weekly summary</p>              <p class="list-description">1.28M requests, 0.02% errors.</p>            </div>            <span class="list-end">Mon</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Cinder is live in 14 regions</p>              <p class="list-description">Deployed from main by Alan.</p>            </div>            <span class="list-end">Sun</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">New sign-in from Tokyo</p>              <p class="list-description">If this was you, there is nothing to do.</p>            </div>            <span class="list-end">Sat</span>          </a>        </li>        <li>          <a class="list-row" href="#">            <div class="list-body">              <p class="list-title">Usage at 80% of your plan</p>              <p class="list-description">Upgrade or lower traffic before the 30th.</p>            </div>            <span class="list-end">Fri</span>          </a>        </li>      </ul>    </div>    <button type="button" class="btn btn-quiet btn-square btn-sm dialog-close" commandfor="inbox-list" command="close" aria-label="Close the inbox">      <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="M6 6l12 12M18 6 6 18" />      </svg>    </button>  </dialog></div>
The list and the message scroll separately. The page does not scroll at all. At Phone the list leaves and a button opens it as a drawer.

#Accessibility

  • Use landmarks: <aside> or <nav> for the sidebar, <main> for the main region, <header> for a header. Label each <nav>; with the drawer there are two, so their labels must differ.
  • The drawer holds the same links as the sidebar, in the same order. While the sidebar is shown, the toggle is display: none and out of the tab order.
  • A region that scrolls by itself and holds nothing focusable needs tabindex="0" and a label, so the keyboard can scroll it.
  • Heights use dvh, so a phone's toolbars never cover the foot of the sidebar; the sidebar also pads its foot by the home indicator's inset.

#Classes

ClassDescription
shellThe frame: a grid at least as tall as the viewport and a size container named shell. It draws nothing
shell-fixedThe shell is exactly the viewport and each region scrolls by itself, for an inbox, an editor or a chat. With shell
shell-headerA header across the top of the shell; the sticky columns start --shell-header under it
shell-sidebarThe column at the start: sticky, as tall as the room left in the viewport, with its own scroll. It leaves the layout below 48rem of shell
shell-mainThe main region: it takes the column that is left and never grows past it
shell-asideA column at the end, sticky like the sidebar. It leaves the layout below 64rem of shell
shell-footerA footer across the foot of the shell, under the columns and below the fold
shell-toggleA control shown only while the sidebar is away, such as the button that opens the same navigation as a drawer

#Custom properties

PropertyDefaultDescription
--shell-sidebar16remThe sidebar's width
--shell-aside14remThe aside's width
--shell-header3.5remThe room kept for a shell-header above the sticky columns; a navbar's height

#Source

@master/ui/layouts/shell.css
/* * Shell: the frame of an app. A sidebar stays in place while the main region moves. An optional header * runs across the top, an optional aside stands at the end, and an optional footer closes the frame. * * By default the document scrolls. The sidebar sticks under the header and is exactly as tall as the room * left in the viewport, measured in dvh, with a scroll of its own; a sticky navbar, anchor links and the * browser's scroll restoration keep working because nothing replaces the page's scroll. * `shell-fixed` takes the viewport itself: only the regions scroll, for an inbox, an editor or a chat. * * The shell is a size container. Below 48rem of shell the sidebar leaves the layout and `shell-toggle` * appears: put the same navigation in a drawer that the toggle opens. The aside leaves first, below 64rem. * * A shell draws nothing. Its surfaces and lines come from components and utilities. */@layer components {  .shell {    --shell-sidebar: 16rem;    --shell-aside: 14rem;    --shell-header: 3.5rem;    /* The room a header takes at the top; the sticky regions start under it. */    --shell-top: 0px;    /* The columns' row is at least the viewport under the header, so a footer starts below the fold. */    --shell-room: calc(100vh - var(--shell-top));    display: grid;    grid-template-rows: auto minmax(var(--shell-room), 1fr) auto;    grid-template-columns: auto minmax(0, 1fr) auto;    align-items: start;    min-width: 0;    container: shell / inline-size;    @supports (height: 100dvh) {      --shell-room: calc(100dvh - var(--shell-top));    }    &:has(.shell-header) {      --shell-top: var(--shell-header);    }    /* Fixed: the shell is the viewport. Each region scrolls by itself and the page never moves. */    &.shell-fixed {      grid-template-rows: auto minmax(0, 1fr) auto;      align-items: stretch;      height: 100vh;      height: 100dvh;      overflow: hidden;      .shell-sidebar, .shell-aside {        position: static;        height: auto;        max-height: none;      }      .shell-main {        min-height: 0;        overflow-y: auto;        overscroll-behavior: contain;      }    }  }  /* The header spans the shell. A navbar is already sticky; any other header stays in the flow. */  .shell-header {    grid-row: 1;    grid-column: 1 / -1;    min-width: 0;  }  /*   * The sidebar and the aside: columns that stay while the page scrolls. Each is as tall as the viewport   * under the header and scrolls by itself, so a long navigation never moves the page.   * The foot keeps clear of the home indicator.   */  .shell-sidebar, .shell-aside {    position: sticky;    top: var(--shell-top);    grid-row: 2;    display: flex;    flex-direction: column;    min-width: 0;    height: var(--shell-room);    padding-bottom: env(safe-area-inset-bottom, 0px);    overflow-y: auto;    overscroll-behavior: contain;    scrollbar-width: thin;  }  .shell-sidebar {    grid-column: 1;    width: var(--shell-sidebar);    @container shell (inline-size < 48rem) {      display: none;    }  }  .shell-aside {    grid-column: 3;    width: var(--shell-aside);    @container shell (inline-size < 64rem) {      display: none;    }  }  /* The main region takes the column that is left, and never grows past it. */  .shell-main {    grid-row: 2;    grid-column: 2;    align-self: stretch;    min-width: 0;  }  /* The footer spans the shell under the columns. It comes into view after them. */  .shell-footer {    grid-row: 3;    grid-column: 1 / -1;    min-width: 0;  }  /* A control that exists only while the sidebar is away, such as the button that opens it as a drawer. */  .shell-toggle {    @container shell (inline-size >= 48rem) {      display: none;    }  }}