Shell
Shell composes navigation, main content and optional context rails. Choose its scroll model first:
page keeps the document scrolling with sticky rails, while content makes the main region scroll.
Examples
A documentation site
This is the page-scroll recipe used by the docs site: a Container bounds the frame, navigation and
contents are named rails, and top={84} keeps them below the sticky site header.
An application workspace
Use scroll="content" for an application frame that owns the viewport height. The main region
scrolls while its navigation rail and top bar stay in place.
These previews render at a fixed 1200px layout width and scale to this column, so they show the wide Shell configuration without horizontal page overflow.
When to use
Use Shell when a page needs persistent navigation or context beside its main task. scroll is not a
visual variant: it selects between two layout engines, so a variant prop would hide a behavior
decision that affects scrolling, sticky positioning and focus travel.
| Property | Default | What it sets |
|---|---|---|
--shell-sidebar | 220px | Sidebar rail width |
--shell-aside | 200px | Complementary rail width |
--shell-top | 0px | Sticky rail offset |
Set top (or --shell-top) to the height of a sticky header. Without it, a page-scroll rail can
hide underneath that header. At 1100px the aside disappears; at 900px the sidebar then stacks
above the document. These breakpoints are fixed because media queries cannot read custom properties.
sidebarWidth controls ordinary sidebar content and page-scroll Shell rails. In a content-scroll
Shell, a direct AppSidebar owns its rail with AppSidebar.width or --appsidebar-width, so the two
width controls never compete.
Reach for something else when:
- The page only needs a centered content measure — use Container.
- The content is one linear sequence without persistent rails — use Stack.
- The main page identity and actions need a heading — use PageHeader inside Shell's content.
Accessibility
- Shell renders one native
<main>by default. For a nested or embedded shell, usemainAs="div"; the page must still have exactly one<main>landmark somewhere. - Use
bannerfor page-level context above the navigation and outside<main>. It renders in a<header>;topbarremains inside the main region for local controls. - Set
sidebarAs="div"when the sidebar content already renders a<nav>, such as AppSidebar. This leaves one navigation landmark instead of nesting it in a complementary landmark. - Set
skipLink={{ label: 'Skip to content' }}to show a link on keyboard focus. Shell targets the main region with a generated id, or usemainIdto supply a stable id. SethrefinskipLinkonly when targeting a different element. sidebarAsandasideAschoosenavoraside. Give each rail a distinctsidebarLabelorasideLabel; a navigation rail should usesidebarAs="nav", as the documentation recipe does.- Shell adds no keyboard shortcuts or focus trap. Links and controls in the rails retain native keyboard behavior, and their DOM order stays sidebar, main, aside at every breakpoint.
- In
scroll="content", ensure the main content contains focusable controls or a managed focus target so keyboard users can enter and discover the scrollable region.
<Shell
banner="Tenant: Acme · Sandbox"
sidebar={<AppSidebar aria-label="Primary" groups={groups} />}
sidebarAs="div"
skipLink={{ label: 'Skip to content' }}
mainId="events"
>
<h1>Events</h1>
</Shell>API and code
| Name | Type | Required | Description |
|---|---|---|---|
sidebar | ReactNode | — | Optional navigation or complementary rail content. |
sidebarAs | 'aside' | 'nav' | 'div' | — | Semantic element for the sidebar rail. Use `"nav"` when it contains primary navigation. |
sidebarLabel | string | — | Accessible name for the sidebar landmark. |
topbar | ReactNode | — | Optional top region placed before the main content. |
banner | ReactNode | — | Page-level banner rendered before the navigation and outside the main landmark. |
skipLink | {
label: string;
href?: string;
} | — | Optional keyboard skip link. Its href defaults to the main region's id. |
mainId | string | — | Id of the main region; generated when a skip link is provided without one. |
mainAs | 'main' | 'div' | — | Semantic element for the main content. Use `"div"` when the shell is nested or embedded inside a page that already owns the `<main>` landmark. |
aside | ReactNode | — | Optional complementary context rail content. |
asideAs | 'aside' | 'nav' | — | Semantic element for the aside rail. Use `"nav"` when it contains navigation. |
asideLabel | string | — | Accessible name for the aside landmark. |
scroll | 'page' | 'content' | — | Whether the document or the main region is the scroll container. Default: `"page"`. |
sidebarWidth | number | — | Sidebar rail width in pixels. Sets `--shell-sidebar` for ordinary rails and page-scroll Shells; a direct AppSidebar in a content-scroll rail owns its width instead. |
asideWidth | number | — | Complementary aside rail width in pixels. Sets `--shell-aside`. |
top | number | — | Sticky rail offset in pixels. Sets `--shell-top`. |
The classes express the page-scroll model. Name both navigation landmarks and set the sticky offset when a fixed site header sits above the frame:
<div
class="lyra-shell lyra-shell--page lyra-shell--has-sidebar lyra-shell--has-aside"
style="--shell-sidebar: 220px; --shell-aside: 200px; --shell-top: 84px"
>
<nav class="lyra-shell__sidebar" aria-label="Documentation navigation">
<a href="/components/container">Container</a>
</nav>
<main class="lyra-shell__main">
<div class="lyra-shell__content">Documentation content</div>
</main>
<nav class="lyra-shell__aside" aria-label="On this page">
<a href="#accessibility">Accessibility</a>
</nav>
</div><lyra:shell> Generated from lyra-ds/blade v0.10.0.
| Prop | Default | Required | Example values |
|---|---|---|---|
sidebarAs | 'aside' | — | — |
asideAs | 'aside' | — | — |
sidebarLabel | null | — | — |
asideLabel | null | — | — |
mainAs | 'main' | — | — |
scroll | 'page' | — | content |
sidebarWidth | null | — | — |
asideWidth | null | — | — |
top | null | — | — |
<lyra:shell sidebar-label="Workspace navigation" main-as="main" scroll="page">
<x-slot:topbar>
<lyra:navbar>
<lyra:nav-link href="/overview" active>Overview</lyra:nav-link>
<lyra:nav-link href="/projects">Projects</lyra:nav-link>
</lyra:navbar>
</x-slot:topbar>
<x-slot:sidebar>
<lyra:nav-link href="/overview" active>Overview</lyra:nav-link>
<lyra:nav-link href="/projects">Projects</lyra:nav-link>
</x-slot:sidebar>
<lyra:page-header title="Overview" />
</lyra:shell>