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>| Attribute | Type | Default | Notes |
|---|---|---|---|
--sk-navbar-height | length | 3.5rem | Set on sk-shell. The sidebar sticks below the navbar, so the two share one measurement rather than restating it. |
data-size | "lg" | 15rem | On 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-label | string | - | 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>| Attribute | Type | Default | Notes |
|---|---|---|---|
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>| Attribute | Type | Default | Notes |
|---|---|---|---|
aria-current | "page" | - | Exactly one per nav. |
sk-nav-label | wrapper | - | Takes the slack and truncates, which is what pushes a trailing badge to the end without a margin. |
sk-nav-section | heading | - | 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>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-divided | "true" | - | A rule underneath, for pages whose content starts immediately rather than inside a card. |
Level | 2 | 3 | 1 | Binding 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>| Attribute | Type | Default | Notes |
|---|---|---|---|
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.
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>