Skip to content

AppShell

The page frame: a sticky sidebar, a sticky topbar and a scrolling main area. Under 768px the sidebar becomes a drawer.

LayoutServer-rendered

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.

Example
<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.

Example
<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.

Example
<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

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

AppShell props
PropTypeDefaultDescription
sidebar*RemixNode–Usually a Sidebar. Placed in a popover <aside>.
topbarRemixNode–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).
navLabelstring'Sidebar'Accessible name of the sidebar <aside>.

Also accepts every <div> attribute. Children render inside <main>.

Sidebar

Sidebar props
PropTypeDefaultDescription
headerRemixNode–Wordmark and workspace switcher.
footerRemixNode–Trunk status, theme toggle, account. Pinned to the bottom.
labelstring'Main'Accessible name of the <nav>.

Also accepts every <div> attribute. Children go inside the <nav>.

SidebarSection

SidebarSection props
PropTypeDefaultDescription
labelstring–Group heading. Only when the sidebar has more than six items.

Also accepts every <div> attribute. Children are NavItems.

NavItem

NavItem props
PropTypeDefaultDescription
href*string–Link target.
label*string–Visible text, truncated with an ellipsis.
iconIconName–Leading 16px icon.
countnumber | string–Trailing count (servers, skills).
currentboolean–Marks the current page with aria-current="page".

Also accepts every <a> attribute. Renders its own <li>.

Topbar

Topbar props
PropTypeDefaultDescription
breadcrumbsRemixNode–Usually Breadcrumbs.
actionsRemixNode–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.