Component
App shell
The frame of a product: a rail of destinations, a top bar and the main column.
Plain-text source, for agents and clipboard use: /components/source/app-shell.txt
When to use it
Holds every screen of an application in one frame, so moving between areas changes the main column and nothing else. The rail lists destinations in labelled sections and marks the current one; it collapses to its icons on a wide screen and opens as a sheet from the top bar on a narrow one. The top bar carries the search entry point, reached with ⌘K, and the session context. One shell replaces the per-product sidebars that each grew their own width, colour and active state.
Examples
Shell with sectioned rail
Property mapping is the current destination. Collapse the rail from its foot; at 48rem and below the rail moves behind the menu button in the top bar. The contained modifier frames the shell for this preview; an application leaves it off and fills the viewport.
Property mapping
Match each supplier property to its Kaptio service before content is imported.
<div class="flux-shell flux-shell--contained" data-flux-shell>
<a class="flux-shell__skip" href="#shell-main">Skip to content</a>
<nav class="flux-shell__rail" id="shell-nav" aria-label="Connect Setup">
<a class="flux-shell__product" href="#">
<span class="flux-shell__mark" aria-hidden="true"></span>
<span class="flux-shell__product-name">Connect Setup<span class="flux-shell__product-meta">Kaptio Edge</span></span>
</a>
<div class="flux-shell__nav">
<div class="flux-shell__section">
<span class="flux-shell__section-label" id="shell-connectors">Connectors</span>
<ul class="flux-shell__list" aria-labelledby="shell-connectors">
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8h9" /></svg><span class="flux-shell__label">New connector</span></a></li>
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4.5h10M3 8h10M3 11.5h6" /></svg><span class="flux-shell__label">Requests</span><span class="flux-shell__count">3</span></a></li>
</ul>
</div>
<div class="flux-shell__section">
<span class="flux-shell__section-label" id="shell-setup">Setup</span>
<ul class="flux-shell__list" aria-labelledby="shell-setup">
<li><a class="flux-shell__item" href="#" aria-current="page"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13.5V5l5-2.5L13 5v8.5M6 13.5V9h4v4.5" /></svg><span class="flux-shell__label">Property mapping</span></a></li>
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="3.5" width="11" height="9" rx="1" /><path d="M2.5 10.5l3-3 2.5 2.5 2-2 3.5 3.5" /></svg><span class="flux-shell__label">Content import</span></a></li>
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="3.5" r="1.5" /><circle cx="3.5" cy="12.5" r="1.5" /><circle cx="12.5" cy="12.5" r="1.5" /><path d="M7.2 4.8l-3 6.2M8.8 4.8l3 6.2M5 12.5h6" /></svg><span class="flux-shell__label">Network import</span></a></li>
</ul>
</div>
<div class="flux-shell__section">
<span class="flux-shell__section-label" id="shell-workspace">Workspace</span>
<ul class="flux-shell__list" aria-labelledby="shell-workspace">
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="2.5" width="9" height="11" rx="1" /><path d="M6 6h4M6 8.5h4M6 11h2" /></svg><span class="flux-shell__label">Plans</span><span class="flux-shell__count">12</span></a></li>
<li><a class="flux-shell__item" href="#"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5.5" cy="10.5" r="2.5" /><path d="M7.3 8.7L13 3M11 5l1.5 1.5" /></svg><span class="flux-shell__label">Credentials</span></a></li>
</ul>
</div>
</div>
<div class="flux-shell__footer">
<button class="flux-shell__item flux-shell__collapse" type="button" aria-label="Collapse navigation" data-flux-shell-collapse><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3.5L5.5 8l4.5 4.5" /></svg><span class="flux-shell__label">Collapse</span></button>
</div>
</nav>
<header class="flux-shell__topbar">
<button class="flux-button flux-button--ghost flux-shell__menu" type="button" aria-label="Open navigation" aria-controls="shell-nav" aria-expanded="false" data-flux-shell-menu><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11" /></svg></button>
<button class="flux-shell__search" type="button" data-flux-shell-search><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="7" cy="7" r="4" /><path d="M10 10l3.5 3.5" /></svg><span class="flux-shell__search-label">Search requests, properties and plans</span><kbd class="flux-shell__kbd" aria-hidden="true">⌘K</kbd></button>
<div class="flux-shell__actions">
<span class="flux-shell__context">demo / staging</span>
<span class="flux-avatar flux-avatar--sm" role="img" aria-label="Orvar Geirsson">OG</span>
</div>
</header>
<main class="flux-shell__main" id="shell-main" tabindex="-1">
<h1>Property mapping</h1>
<p>Match each supplier property to its Kaptio service before content is imported.</p>
</main>
</div>Anatomy
| Part | Description |
|---|---|
| Skip link | The first focusable element. Hidden until focused, then jumps to the main column. |
| Rail | A nav labelled with the product name, on layer-01 with a hairline against the main column. |
| Product | The product mark, filled with its --flux-product-* accent, and the product name. Links to the product home. |
| Sections | Groups of destinations, each with a short mono label. Three or four sections at most. |
| Item | An icon, a label and an optional count. The current destination carries aria-current="page". |
| Collapse | At the foot of the rail. Reduces the rail to its icons on a wide screen. |
| Top bar | The menu button on a narrow screen, the search entry point, and the session context with the avatar. |
| Main | The page itself: its h1, then its content. It scrolls beneath the sticky top bar. |
API
| Name | Kind | Description |
|---|---|---|
flux-shell | class | The grid. Fills the viewport and holds the rail, the top bar and the main column. |
data-flux-shell | attribute | Wires collapse, the narrow sheet and the search shortcut. Requires flux-components.js. |
flux-shell--contained | class | Frames the shell at --flux-shell-contained-height for previews and documentation. The search shortcut is not bound, so the host page keeps its own. |
data-collapsed | attribute | On flux-shell. Set it at render time to start collapsed, for example from a saved preference. |
flux-shell__skip | class | The skip link. Its href names the main column. |
flux-shell__rail | class | The nav element. Give it an id for the menu button’s aria-controls and an aria-label with the product name. |
flux-shell__product | class | The product link, with flux-shell__mark and flux-shell__product-name inside. |
flux-shell__mark | class | The square mark. Set --flux-shell-mark to the product’s accent, such as var(--flux-product-edge). |
flux-shell__product-meta | class | Optional span inside the product name, such as Kaptio Edge. |
flux-shell__nav | class | The scrolling list of sections. |
flux-shell__section | class | One group: a flux-shell__section-label span and a flux-shell__list ul labelled by it. |
flux-shell__item | class | A link with an svg icon, a flux-shell__label span and an optional flux-shell__count span. |
aria-current="page" | attribute | On the item for the current destination. Fills the item and sets its label in bold. |
flux-shell__footer | class | Holds the collapse button, a flux-shell__item with flux-shell__collapse and data-flux-shell-collapse. |
flux-shell__topbar | class | The header element. |
data-flux-shell-menu | attribute | On a ghost Button with flux-shell__menu, aria-controls and aria-expanded. Shown at 48rem and below. |
data-flux-shell-search | attribute | On the flux-shell__search button. Click and ⌘K or Ctrl K dispatch a bubbling flux-shell-search event; open the product’s search in a Modal from it. |
flux-shell__actions | class | The end of the top bar: flux-shell__context in mono and an Avatar. |
flux-shell__main | class | The main element, with the id the skip link names and tabindex="-1". |
States
| State | Treatment |
|---|---|
| Current | aria-current="page" fills the item in surface-selected, sets the label in bold text-brand and the icon in icon-brand. Weight and fill change, so colour is not the only signal. |
| Hover | Background layer-hover-01 and text-primary. Colour only; nothing moves. |
| Collapsed | The rail narrows to --flux-shell-rail-collapsed-width. Labels, counts and section labels stay in the accessibility tree; each item gains a title with its label, and a hairline separates the sections. |
| Narrow | At 48rem and below the rail leaves the grid. The menu button opens it as a sheet over a backdrop, with --flux-shell-sheet-shadow because it floats. The top bar and main column become inert while it is open. |
Keyboard
| Keys | Action |
|---|---|
Tab | Skip link first, then the rail, the top bar and the main column, in that order. |
Enter | On the skip link, moves focus to the main column. |
⌘K / Ctrl K | Activates the search entry point from anywhere on the page. |
Escape | Closes the narrow sheet and returns focus to the menu button. |
Accessibility
- Use nav, header and main, so each region is a landmark. Label the nav with the product name.
- Set aria-current="page" on exactly one item, and update it on every client-side navigation.
- Label each list by its section label with aria-labelledby, so "Setup, list, 3 items" is announced.
- Keep labels in the accessibility tree when collapsed. The rail hides them visually, never with display: none.
- Opening the sheet moves focus to the current item; closing it returns focus to the menu button.
Tokens
The tier-3 and tier-2 tokens this component binds to. Change the token, not the component.
- --flux-shell-rail-width | -rail-collapsed-width | -topbar-height | -search-width | -contained-height
- --flux-shell-rail-bg | -topbar-bg | -main-bg | -divider | -backdrop | -sheet-shadow
- --flux-shell-padding | -content-padding | -section-gap | -section-label
- --flux-shell-item-height | -item-gap | -item-padding-x | -item-radius | -icon-size
- --flux-shell-item-text | -item-hover | -item-hover-text | -item-current-bg | -item-current-text | -item-icon | -item-current-icon
- --flux-shell-mark | -mark-size
Do and don’t
Do
- Name destinations by what the user finds there, such as Requests or Plans, not by the team that owns them.
- Remember the collapsed state per user, and render data-collapsed on the server or before first paint so the rail does not jump.
- Show a count only when it is work waiting for the user, such as open requests.
- Put the page title in the main column’s h1, where it scrolls with the page, and keep the top bar for search and context.
Don’t
- Do not number rail items. A rail is a set of places; a sequence belongs in a Flow.
- Do not set the rail on a dark or brand ground. It is layer-01, like every other surface around content.
- Do not put a count in a pill or a badge. A count is not a status.
- Do not hide items for a role by greying them out. Leave them out, or keep them and explain on the destination why it is locked.
- Do not animate the rail’s width when it collapses. It changes in one step.