Skip to content
  1. Keyline UI
  2. App shell
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

App shell

The page around an app's screens: a header frame, the sidebar, the page and an optional aside, one gutter apart. It lays itself out by its own width, not the window's, so it works in a pane too. Wide, everything sits side by side. Narrower, the aside opens as a sheet. Narrowest, the sidebar does too.

Loading the live example…

"use client"

import { useRef, useState } from "react"
import { AppShell } from "@/components/keyline/ui/app-shell"
import { Breadcrumb } from "@/components/keyline/ui/breadcrumb"
import { Button } from "@/components/keyline/ui/button"
import { Inspector, InspectorActions, InspectorBody, InspectorFacts, InspectorHeader } from "@/components/keyline/ui/inspector"
import { Item, ItemContent, ItemList, ItemMedia } from "@/components/keyline/ui/item"
import { Nav } from "@/components/keyline/ui/nav"
import { Status } from "@/components/keyline/ui/status"

const VIEWS = { orders: "Orders", clients: "Clients", warehouse: "Warehouse" } as const
type View = keyof typeof VIEWS

const ORDERS = [
  { id: "DM-4107", client: "Lackawanna County", cases: 120, rep: "Dwight Schrute" },
  { id: "DM-4108", client: "Blue Cross of Pennsylvania", cases: 60, rep: "Jim Halpert" },
  { id: "DM-4109", client: "Vance Refrigeration", cases: 12, rep: "Phyllis Vance" },
]

export function OfficeShell() {
  const [view, setView] = useState<View>("orders")
  const [open, setOpen] = useState<string | null>(null)
  const opener = useRef<HTMLElement | null>(null)
  const order = ORDERS.find((o) => o.id === open)

  return (
    <AppShell
      // a low breakpoint keeps the sidebar in a preview this narrow
      narrowAt={400}
      mainProps={{ className: "min-h-[300px]" }}
      navSize={180}
      navLabel="Scranton branch"
      nav={
        <Nav
          layout="cell"
          label="Scranton branch"
          value={view}
          onValueChange={(v) => setView(v as View)}
          items={Object.entries(VIEWS).map(([value, label]) => ({ value, label }))}
        />
      }
      header={<Breadcrumb layout="cell" items={[{ label: "Scranton", render: <a href="?view=orders" onClick={(e) => (e.preventDefault(), setView("orders"))} /> }, { label: VIEWS[view] }]} />}
      actions={<Button layout="cell" variant="primary">New order</Button>}
      aside={
        order ? (
          <Inspector>
            <InspectorHeader code={order.id} title={order.client} status={<Status kind="normal">Out for delivery</Status>} onClose={() => setOpen(null)} />
            <InspectorBody>
              <InspectorFacts
                items={[
                  { label: "Cases", value: order.cases },
                  { label: "Rep", value: order.rep },
                ]}
              />
            </InspectorBody>
            <InspectorActions>
              <Button layout="cell" onClick={() => setOpen(null)}>
                Close
              </Button>
            </InspectorActions>
          </Inspector>
        ) : null
      }
      asideLabel="Order"
      asideOpen={!!order}
      onAsideOpenChange={(o) => (o ? null : setOpen(null))}
      asideFinalFocus={opener}
    >
      {view === "orders" ? (
        <ItemList aria-label="Open orders">
          {ORDERS.map((o) => (
            <Item
              key={o.id}
              current={o.id === open}
              render={<button type="button" />}
              onClick={(e) => {
                opener.current = e.currentTarget
                setOpen(o.id)
              }}
            >
              <ItemMedia>{o.id.slice(3)}</ItemMedia>
              <ItemContent title={o.client} description={o.rep} />
            </Item>
          ))}
        </ItemList>
      ) : (
        <p className="m-0 text-[14px] text-kl-ink-2">{view === "clients" ? "64 active clients." : "88 pallets on the floor."}</p>
      )}
    </AppShell>
  )
}

Installation

npx shadcn@latest add @keyline-ui/app-shell

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { AppShell } from "@/components/keyline/ui/app-shell"
<AppShell
  navLabel="Scranton branch"
  nav={<Nav layout="cell" label="Scranton branch" items={…} />}
  header={<Breadcrumb layout="cell" items={…} />}
  actions={<Button layout="cell" variant="primary">New order</Button>}
  aside={<Inspector>…</Inspector>}
  asideOpen={!!order}
  onAsideOpenChange={…}
>
  …the page
</AppShell>

Props

PropTypeDefaultDescription
navReactNodeDefault —The sidebar, usually Nav layout="cell".
navLabelstringDefault —Names the sidebar: the toggle says &ldquo;Show&rdquo; it, and its narrow sheet takes the name.
navOpen · defaultNavOpen · onNavOpenChangebooleanDefault trueWhether the sidebar column shows on a wide layout.
collapse"hide" | "rail"Default "hide"What closing the sidebar does. rail keeps a column of icons. See Sidebar.
header · actionsReactNodeDefault —The header after the toggle (usually a Breadcrumb) and the cells at its end (a Button, or a Toolbar with overflow="menu").
aside · asideOpen · onAsideOpenChangeReactNode · booleanDefault —A panel at the end, such as an Inspector, shown while open.
asideFinalFocusrefDefault —Where focus goes when the aside&apos;s sheet closes. Pass the control that opened it.
narrowAt · asideInlineAtnumberDefault 960 · narrowAt + 440Shell widths where the sidebar and the aside turn into sheets.
navSize · asideSizenumber | stringDefault 15rem · 22remColumn widths on a wide layout.
stickyHeaderbooleanDefault falseKeeps the header in view; the columns stay put while the page scrolls past.
mainPropsprops of <main>Default —For the page&apos;s main element.

The example sets narrowAt low so the sidebar stays in a preview this size. Choose an order to open the aside: on this page's width it opens as a sheet. This site's docs pages are a Keyline AppShell.

Sheet 57 of 73PreviousNoticeNextSidebar
On this page