AppShell
The page frame: a sticky sidebar, a sticky topbar and a scrolling main area. Under 768px the sidebar becomes a drawer.
Examples
Sidebar
The shell itself is not rendered here: this site is built on it, so look at the frame around this page. Below is a Sidebar on its own, at the shell's 232px width.
<Sidebar footer={<Lamp state="live" label="Trunk live" showLabel />}>
<SidebarSection>
<NavItem href="/servers" icon="server" label="Servers" count={4} current />
<NavItem href="/skills" icon="book-open" label="Skills" count={2} />
<NavItem href="/agents" icon="plug" label="Agents" />
</SidebarSection>
<SidebarSection>
<NavItem href="/settings" icon="settings" label="Settings" />
</SidebarSection>
</Sidebar>Labelled sections
Give sections a label only when the sidebar has more than six items. Groups are separated by space, not rules.
<SidebarSection label="Connect">
<NavItem href="/servers" icon="server" label="Servers" count={4} />
<NavItem href="/skills" icon="book-open" label="Skills" count={2} />
</SidebarSection>
<SidebarSection label="Observe">
<NavItem href="/activity" icon="activity" label="Activity" current />
<NavItem href="/routes" icon="route" label="Routes" />
</SidebarSection>Topbar
Breadcrumbs on the left, a few quiet actions on the right. The shell adds the phone menu button in front.
<Topbar
breadcrumbs={<Breadcrumbs items={[{ label: 'Servers', href: '/servers' }, { label: 'GitHub' }]} />}
actions={
<>
<IconButton icon="search" label="Search" />
<IconButton icon="book-open" label="Docs" />
<Avatar name="Ryan Park" size={24} />
</>
}
/>Usage
import { AppShell, Sidebar, SidebarSection, NavItem, Topbar } from '@switchboard/ui'
<AppShell
sidebar={
<Sidebar header={<Wordmark />} footer={<TrunkStatus />}>
<SidebarSection>
<NavItem href="/servers" icon="server" label="Servers" count={4} current />
<NavItem href="/skills" icon="book-open" label="Skills" count={2} />
</SidebarSection>
</Sidebar>
}
topbar={<Topbar breadcrumbs={<Breadcrumbs items={crumbs} />} />}
>
<PageHeader title="Servers" />
…
</AppShell>Props
Every component also takes mix, for css() and mixins applied after its own style.
AppShell
| Prop | Type | Default | Description |
|---|---|---|---|
| sidebar* | RemixNode | – | Usually a Sidebar. Placed in a popover <aside>. |
| topbar | RemixNode | – | Usually a Topbar. The shell adds the phone menu button before it. |
| width | 'default' | 'narrow' | 'full' | 'default' | Max content width in main: 1200px, 760px (forms) or full (wide tables). |
| navLabel | string | 'Sidebar' | Accessible name of the sidebar <aside>. |
Also accepts every <div> attribute. Children render inside <main>.
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
| header | RemixNode | – | Wordmark and workspace switcher. |
| footer | RemixNode | – | Trunk status, theme toggle, account. Pinned to the bottom. |
| label | string | 'Main' | Accessible name of the <nav>. |
Also accepts every <div> attribute. Children go inside the <nav>.
SidebarSection
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | – | Group heading. Only when the sidebar has more than six items. |
Also accepts every <div> attribute. Children are NavItems.
NavItem
| Prop | Type | Default | Description |
|---|---|---|---|
| href* | string | – | Link target. |
| label* | string | – | Visible text, truncated with an ellipsis. |
| icon | IconName | – | Leading 16px icon. |
| count | number | string | – | Trailing count (servers, skills). |
| current | boolean | – | Marks the current page with aria-current="page". |
Also accepts every <a> attribute. Renders its own <li>.
Topbar
| Prop | Type | Default | Description |
|---|---|---|---|
| breadcrumbs | RemixNode | – | Usually Breadcrumbs. |
| actions | RemixNode | – | Search trigger, docs link, avatar. Right-aligned. |
Also accepts every <div> attribute. Children render after the breadcrumbs.
Accessibility
- The first focusable element is a "Skip to content" link to <main>, which has tabindex="-1" so it can take focus.
- On phones the sidebar is a native popover: the menu button opens it, Esc and an outside click close it, with no JavaScript.
- The sidebar is an <aside> named by navLabel, and its links sit in a <nav> named by the Sidebar label.
- NavItem sets aria-current="page" on the current link; its count is read as "4 items".
Guidance
Do
- Use one AppShell per page, at the root of the layout.
- Mark exactly one NavItem as current.
- Use width="narrow" on form pages and "full" for wide tables.
Don’t
- Nest an AppShell inside another; it duplicates the landmarks and the drawer.
- Put primary page actions in the topbar; they belong in the PageHeader.