App shell

The frame every application draws around everything else: a navbar, a sidebar, a page header and a toolbar. This kitchen sink has had all four since the first commit, in its own private stylesheet, and so did every application that used the system - which is the usual sign that something belongs in it.

Reference

sk-shell, sk-navbar, sk-sidebar

No menu button. Opening a panel needs script and this system ships none, so below 60rem the sidebar becomes a horizontal strip of the same links above the content: every destination stays reachable, in the same order, with no state to hold and nothing to get stuck open. An application that wants a slide-out already has one - Drawer is a <dialog> with a declarative trigger.

<div class="sk-shell">
  <header class="sk-navbar">…</header>
  <div class="sk-shell-body">
    <aside class="sk-sidebar"><nav class="sk-nav">…</nav></aside>
    <main class="sk-shell-main">…</main>
  </div>
</div>
AttributeTypeDefaultNotes
--sk-navbar-heightlength3.5remSet on sk-shell. The sidebar sticks below the navbar, so the two share one measurement rather than restating it.
data-size"lg"15remOn the sidebar. 18rem, for labels that would otherwise truncate.
data-align"end"-On sk-navbar-items. Pushes the account menu to the trailing edge without a spacer element.
aria-labelstring-On each nav. A page with a sidebar and a breadcrumb otherwise gives a screen reader two landmarks both called "navigation".

sk-skip-link

The first focusable thing in the document, and for most readers the one component here they will never see: without it a keyboard or switch user tabs through every destination in the navbar and the sidebar before reaching the page, on every page, every time. It is MOVED rather than visually hidden - the 1px-clip technique cannot come back into view - and it keys off :focus rather than :focus-visible, because there is nothing to click until it is focused, so the distinction does not exist here.

<a class="sk-skip-link" href="#main">Skip to content</a>
…
<main class="sk-shell-main" id="main">…</main>
AttributeTypeDefaultNotes
href#id-ShellMain carries id="main" already, so the pair needs no wiring. A fixed id rather than a prop: the accessibility feature most likely to be skipped is the one needing two components to agree on a string.
tabindex--Not needed on the target. <main> is a landmark and browsers move focus into it on a fragment navigation; adding tabindex="-1" to a large scrolling region makes it clickable-focusable too, which is a stray tab stop.

sk-nav-item

A link, not a button with a handler: a destination is a URL, so it opens in a new tab, bookmarks, and works with the back button. The current item carries aria-current="page", which is both the announcement and the styling hook - so an item cannot look current without being announced as current. It is filled rather than merely recoloured, because a colour change alone is weak in a column of otherwise identical rows.

<a class="sk-nav-item" href="…" aria-current="page">
  <svg class="sk-icon">…</svg>
  <span class="sk-nav-label">Builds</span>
  <span class="sk-badge">3</span>
</a>
AttributeTypeDefaultNotes
aria-current"page"-Exactly one per nav.
sk-nav-labelwrapper-Takes the slack and truncates, which is what pushes a trailing badge to the end without a margin.
sk-nav-sectionheading-Hidden on narrow screens, where the nav is one row and a heading would be a word floating between two links.

sk-page-header

Actions stay on the first line of the title rather than centred against the whole heading block - a Save button floating in the middle of a two-line title is the defect this exists to prevent. The heading gets flex: 1 1 20ch and min-inline-size: 0, so a long title wraps instead of pushing the actions off the end.

<div class="sk-page-header">
  <div class="sk-page-heading">
    <h1 class="sk-page-title">Builds</h1>
    <p class="sk-page-subtitle">All builds for Kiwi Kart.</p>
  </div>
  <div class="sk-page-actions">…</div>
</div>
AttributeTypeDefaultNotes
data-divided"true"-A rule underneath, for pages whose content starts immediately rather than inside a card.
Level2 | 31Binding only. Two h1s on a page is a real defect for anyone navigating by heading, and that is what goes wrong first when this is reused half way down a screen.

sk-toolbar

NOT role="toolbar". That role promises arrow-key navigation between the controls and one tab stop for the group, all of which needs script - the same reasoning that keeps Tabs a list of links. It wraps rather than scrolls, too: a toolbar that runs off the edge hides the filter that is currently on, which is the one piece of state the reader most needs to see.

<div class="sk-toolbar" role="group" aria-label="Filters">
  <div class="sk-toolbar-group" data-grow="true">…</div>
  <span class="sk-toolbar-separator" aria-hidden="true"></span>
  <div class="sk-toolbar-group">…</div>
</div>
AttributeTypeDefaultNotes
data-variant"bare"-Drops the border and background, for a toolbar directly above a card that already has both.
data-grow"true"-On one group. Takes the slack, so everything after it sits on the trailing edge with no spacer.

Examples

The shell

A miniature application. The navbar sticks, the sidebar sticks under it, and the content column scrolls. Drag the preview narrow: past 60rem the sidebar becomes a strip above the content. Tab into it and the skip link appears first.

Skip to content
Kiwi Ops
Ada Lovelace

Overview

Everything shipping this week.

Passing

398

Failed

14

Show markup
<div class="sink-frame">
  <div class="sk-shell">
    <a class="sk-skip-link" href="#main">
      Skip to content
    </a>
    <header class="sk-navbar">
      <a class="sk-navbar-brand" href="#shell">
        <span class="sk-mark" data-fill="true">
          Kiwi
        </span>
        Ops
      </a>
      <div class="sk-navbar-items" data-align="end">
        <button class="sk-button" type="button" data-variant="quiet" data-size="sm" aria-label="Search">
          <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
            <circle cx="11" cy="11" r="7">
            </circle>
            <path d="m20 20-3.5-3.5">
            </path>
          </svg>
        </button>
        <span class="sk-avatar" data-size="sm">
          <span aria-hidden="true">
            AL
          </span>
          <span class="sk-visually-hidden">
            Ada Lovelace
          </span>
        </span>
      </div>
    </header>
    <div class="sk-shell-body">
      <aside class="sk-sidebar">
        <nav class="sk-nav" aria-label="Main">
          <a class="sk-nav-item" href="#shell" aria-current="page">
            <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M20 6 9 17l-5-5">
              </path>
            </svg>
            <span class="sk-nav-label">
              Overview
            </span>
          </a>
          <a class="sk-nav-item" href="#shell">
            <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="m9 18 6-6-6-6">
              </path>
            </svg>
            <span class="sk-nav-label">
              Builds
            </span>
            <span class="sk-badge" data-status="neutral">
              3
            </span>
          </a>
          <p class="sk-nav-section">
            Settings
          </p>
          <a class="sk-nav-item" href="#shell">
            <span class="sk-nav-label">
              Team
            </span>
          </a>
          <a class="sk-nav-item" href="#shell">
            <span class="sk-nav-label">
              Billing
            </span>
          </a>
        </nav>
      </aside>
      <main class="sk-shell-main" id="main">
        <div class="sk-flex sk-flex-col sk-gap-5">
          <div class="sk-page-header">
            <div class="sk-page-heading">
              <h2 class="sk-page-title">
                Overview
              </h2>
              <p class="sk-page-subtitle">
                Everything shipping this week.
              </p>
            </div>
            <div class="sk-page-actions">
              <button class="sk-button" type="button" data-variant="solid" data-size="sm">
                New build
              </button>
            </div>
          </div>
          <div class="sk-stat-group">
            <div class="sk-stat" data-size="sm">
              <p class="sk-stat-label">
                Passing
              </p>
              <p class="sk-stat-value">
                398
              </p>
            </div>
            <div class="sk-stat" data-size="sm">
              <p class="sk-stat-label">
                Failed
              </p>
              <p class="sk-stat-value">
                14
              </p>
            </div>
          </div>
        </div>
      </main>
    </div>
  </div>
</div>

Page header

With and without a rule. The second one has a title long enough to wrap - the actions stay on the first line.

Builds

All builds for Kiwi Kart, newest first.

A title long enough that it has to wrap in a narrow column

Show markup
<div class="sk-flex sk-flex-col sk-gap-7">
  <div class="sk-page-header" data-divided="true">
    <div class="sk-page-heading">
      <h2 class="sk-page-title">
        Builds
      </h2>
      <p class="sk-page-subtitle">
        All builds for Kiwi Kart, newest first.
      </p>
    </div>
    <div class="sk-page-actions">
      <button class="sk-button" type="button" data-variant="neutral" data-size="md">
        Export
      </button>
      <button class="sk-button" type="button" data-variant="solid" data-size="md">
        New build
      </button>
    </div>
  </div>
  <div class="sk-page-header">
    <div class="sk-page-heading">
      <h2 class="sk-page-title">
        A title long enough that it has to wrap in a narrow column
      </h2>
    </div>
    <div class="sk-page-actions">
      <button class="sk-button" type="button" data-variant="neutral" data-size="md">
        Save
      </button>
    </div>
  </div>
</div>

Toolbar

Filters take the slack, actions sit at the end. Narrow the preview and it becomes two rows rather than a scroll container.

Show markup
<div class="sk-toolbar" role="group" aria-label="Build filters">
  <div class="sk-toolbar-group" data-grow="true">
    <input class="sk-input" id="toolbar-q" name="toolbar-q" type="search" value="" placeholder="Search builds" data-size="sm" aria-invalid="false" aria-label="Search builds">
    <div class="sk-field">
      <select class="sk-select" id="toolbar-status" name="toolbar-status" data-size="sm" aria-invalid="false" aria-label="Status">
        <option value="all">
          All statuses
        </option>
        <option value="passing">
          Passing
        </option>
        <option value="failed">
          Failed
        </option>
      </select>
    </div>
  </div>
  <span class="sk-toolbar-separator" aria-hidden="true">
  </span>
  <div class="sk-toolbar-group">
    <button class="sk-button" type="button" data-variant="neutral" data-size="sm">
      Export
    </button>
    <button class="sk-button" type="button" data-variant="solid" data-size="sm">
      New build
    </button>
  </div>
</div>