Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

App shell

The frame of a whole app, as an editor or a dashboard, and the page that goes in it.

The app shell is the frame of a whole app screen, an editor's or a dashboard's. Use AppShell for the screen and AppPage for each page inside a dashboard.

  • An editor

    The default, layout="inspector": a header and a stage beside an inspector, which becomes a sheet on a narrow screen. The Inspector pattern has its parts.

  • A dashboard

    With layout="sidebar": a sidebar beside a top bar and the page, an AppPage, and an optional SidePanel at the shell's end.

The dashboard

The shell never scrolls; the sidebar and the page each scroll on their own. The sidebar and a SidePanel change with the shell's width:

Shell widthSidebarSidePanel
1280px and widerIn fullDocked at the shell's end
1100px to 1280pxIn fullOpens as a sheet
640px to 1100pxAn icon railOpens as a sheet
Under 640pxA sheet, opened from the top bar's menu buttonOpens as a sheet

Give the shell a height. AppShell fills its container's height, so the element around it needs one. In an app that's the window's height, which the templates give their root element in src/app.css. In a container with no height of its own, the shell grows with its content, and the sidebar and the page stop scrolling on their own.

src/app.css

.app {
  block-size: 100dvh;
}

A dashboard with two pages in its sidebar:

src/Dashboard.tsx

import type { ReactElement } from "react";
import { Package, Receipt } from "lucide-react";
import {
  AppPage,
  AppShell,
  PageHeader,
  Sidebar,
  TopBar,
} from "@butteryui/react";
import "./app.css";

export function Dashboard(): ReactElement {
  return (
    <div className="app">
      <AppShell
        layout="sidebar"
        sidebar={
          <Sidebar label="Main">
            <Sidebar.Item
              label="Orders"
              href="/orders"
              icon={Receipt}
              current
            />
            <Sidebar.Item label="Stock" href="/stock" icon={Package} />
          </Sidebar>
        }
        topBar={<TopBar />}
      >
        <AppPage header={<PageHeader title="Orders" />}>
          <AppPage.Section>Today's orders</AppPage.Section>
        </AppPage>
      </AppShell>
    </div>
  );
}

AppShell

When to use it

Use it for

  • An editor or a tool: a document's header, its canvas, and the settings for it
  • A dashboard: layout="sidebar" with a Sidebar, a TopBar and an AppPage for each page

Not for

  • A marketing or content page: use Page and Section, with a NavBar
  • A settings screen with no canvas: an Inspector alone, or a form

Examples

Editor screen

A small editor: header, stage and inspector, with the inspector driving the stage's shape.

Spring gift card

$40 of Halfmoon coffee

Format

Shape1600 × 900

Phone sheet

At phone width the inspector opens as a sheet; with open kept in useState, Done in the sheet closes it.

Gift card for Theo

Happy birthday, Theo

Front

Roastery dashboard

A dashboard: the sidebar beside the top bar and the page, the week's numbers and today's orders, which a control filters, beside a rail of what needs attention, and the top bar's create button adding an order.

Orders

3

The week so far, and what came in today.

This week

Orders

48

Bags roasted

212

Sales

$3,940

Today

Order 1053 · Blue Door Café

Paid

Order 1052 · Ines Vale

Roasting

Order 1051 · Pier 9 Kiosk

Shipped

Variants and states

Editor

The header over the stage and the inspector beside them; narrower than 840px, the inspector opens as a sheet from the Edit button.

Gift card for Rosa

Happy birthday, Rosa

Message

Text size80%

Props

AppShell

PropTypeDefaultDescription
layout"inspector" | "sidebar""inspector""inspector" (the default) for an editor: the work area with an inspector beside it. "sidebar" for a dashboard: a sidebar beside a column holding the top bar and the page.
header

required

ReactNodeWith layout="inspector": The bar at the top of the work area, such as a HeaderBar.
inspector

required

ReactNodeWith layout="inspector": The settings beside the work area, such as an Inspector. On narrow screens it opens as a sheet.
inspectorLabel

required

stringWith layout="inspector": Names the inspector's sheet on narrow screens, such as "Gift card settings".
openInspectorLabelstring"Edit"With layout="inspector": The button that opens the sheet on narrow screens. Defaults to "Edit".
openbooleanWith layout="inspector": Whether the inspector's sheet is open on a narrow screen. Pair with onOpenChange to control it. With layout="sidebar": Whether the sidebar's sheet is open on a narrow screen. Pair with onOpenChange to control it.
defaultOpenbooleanfalseWhether the sheet starts open when uncontrolled. Defaults to false.
onOpenChange((open: boolean) => void)With layout="inspector": Called when the sheet opens or closes, including when the screen widens and the inspector moves back beside the stage. With layout="sidebar": Called when the sheet opens or closes, including when the shell widens and the sidebar comes back.
children

required

ReactNodeWith layout="inspector": The work area, such as a Stage. With layout="sidebar": The page, an AppPage.
sidebar

required

ReactNodeWith layout="sidebar": The app's navigation, a Sidebar. It becomes an icon rail under 1100px of shell and a sheet under 640.
topBarReactNodeWith layout="sidebar": The bar over the page, a TopBar.
panelReactNodeWith layout="sidebar": A SidePanel, such as help, at the shell's end: docked beside the column from 1280px of shell, making the page narrower, and a sheet under that.
sidebarLabelstring"Navigation"With layout="sidebar": Names the sidebar's sheet on narrow screens. Defaults to "Navigation".
skipLabelstring"Skip to content"With layout="sidebar": Names the button that is the first thing Tab reaches, shown only while it has focus, which moves focus past the sidebar and the top bar to the page. Defaults to "Skip to content".

It also takes Omit<ComponentPropsWithRef<"div">, "children">.

Also exported

ExportDescription
AppShellContext: Context<AppShellState>The state an AppShell layout="sidebar" shares. Outside one, the sidebar is full and nothing opens it, and a side panel docks where it's placed.
useAppShell(): AppShellStateReads the sidebar shell around a component: how its sidebar shows and how to open its sheet.

Accessibility

In the editor layout the stage is the page's main landmark; in the sidebar layout the AppPage inside is. The parts keep their own semantics (the header, the stage's image, the inspector's region). On a narrow screen the inspector opens as a modal sheet named by inspectorLabel, from a button under the stage; Escape or a press on the scrim closes it and focus goes back to the button. In the sidebar layout the sidebar is a rail of named icons under 1100px and, under 640, a modal sheet named by sidebarLabel that the top bar's menu button opens (aria-haspopup, aria-expanded); it closes on a link, Escape or the scrim, with focus back on that button, or on the sidebar's current page if the shell widened.

KeysWhat they do
TabIn the sidebar layout, reaches Skip to content first; pressing it moves focus past the sidebar and the top bar to the page
Enter or SpaceOpens the inspector's sheet from the button under the stage, or the sidebar's sheet from the top bar's menu button, on a narrow screen
Tab or Shift+TabMoves through an open sheet's controls without leaving it
EnterFollows a link in the sidebar's sheet and closes the sheet
EscapeCloses the open sheet and puts focus back on the button that opened it

Motion

It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.

With reduced motion: The inspector's and the sidebar's sheets fade in and out in 100ms with their scrims instead of rising.

Layout

It fills its container's height, so give the element around it one: in an app, the window's, as the templates do with block-size: 100dvh on their root element; in a container with no height of its own it grows with its content, and its parts stop scrolling on their own. The editor is at most 1380px wide (--bt-w-shell-max), centered, padded and spaced 8, with the 340px inspector (--bt-w-inspector) beside the header and stage, or under 840px a bottom sheet opened by a full-width 48px button under the stage. With layout="sidebar" the 240px sidebar (--bt-w-sidebar) sits beside the top bar and the page, a 56px rail under 1100px of shell and a sheet under 640, and only the sidebar and the page scroll.

See it in the roastery demo. See it in the Deckhand demo.

AppPage

When to use it

Use it for

  • Every page inside an AppShell layout="sidebar"
  • A one-page app without navigation, where it frames itself
  • A detail page with a right rail of facts beside it
  • A page whose table should fill the height (fill)

Not for

  • A marketing page: use Page with Section
  • An editor's work area: use AppShell's inspector layout with a Stage

Examples

Customer page

A customer's page in a dashboard shell: sections with an action, a right rail of facts and a small footer.

Ines Vale

14 orders

Orders

Order 1051, roasting

Order 1053, paid

Subscription

A bag of the house blend every two weeks.

Send feedback about this page

Orders page

A contained page: its header, then sections 32px apart, the first with an action at the end of its heading.

Orders

1,284

Needs attention

Three orders are waiting on beans.

This week

212 orders shipped.

Variants and states

Header, sections and a right rail

Sections sit 24px under the header and 32px apart; the rail moves beside them in a wide shell.

Ines Vale

14 orders

Orders

The last order shipped on Tuesday.

Notes

Prefers a lighter roast.

Props

AppPage

PropTypeDefaultDescription
width"contained" | "medium" | "narrow" | "full""contained"How wide the page gets, its padding included, centered in the column: "contained" (the default), 1280px, for index and table pages; "medium", 960px, for a settings detail page; "narrow", 820px, for a dense form or a preferences matrix; or "full", the column's width.
headerReactNodeThe page's header, a PageHeader, with its tabs and quick actions inside it.
asideReactNodeA right rail beside the page, 320px wide from 1040px of the shell's column beside a full sidebar (1280 of shell, with no side panel docked), under the page otherwise, including beside a narrow shell's icon rail.
asideLabelstring"Details"Names the right rail's landmark, such as "Customer details". Defaults to "Details".
fillbooleanfalseGives the page's last block the rest of the height, so a table scrolls inside it with its header sticky while the header and filters stay put; the table's pagination then keeps floating lanes above it. Defaults to false.
footerReactNodeA small footer that ends the page under a hairline, such as a feedback link and the keyboard shortcuts, in body-sm and text-secondary.

It also takes ComponentPropsWithRef<"main">.

AppPage.Section

PropTypeDefaultDescription
titlestringThe section's heading, an h2 in title-md, which names it.
descriptionReactNodeOne line under the heading saying what the section is for, in body-sm and text-secondary.
actionsReactNodeActions at the end of the heading's row, such as a small "See all" button.

It also takes Omit<MotionElementProps<"section">, "title">.

Also exported

ExportDescription
AppPageContext: Context<AppPageState | null>The state an AppPage shares, or null outside one.
useAppPage(): AppPageState | nullReads the AppPage around a component, or null outside one.

Accessibility

The page's main landmark. Each titled section is a region named by its h2, its description a paragraph under it; the right rail is a complementary landmark named by asideLabel; the footer is a footer element after the page's content.

It takes no keyboard input of its own.

Motion

It changes shape as one surface, on `spring.smooth`, or `spring.morph` for a big change, rather than closing and reopening.

With reduced motion: When a banner or card above a section is dismissed, the sections after it take their new places at once instead of sliding up.

Layout

It centers in its column at most 1280px wide with its padding (--bt-w-app-page; 960 with width="medium", 820 with "narrow", the whole column with "full"), padded 32, 24 or 16 by the column's width, its sections 24 under the header and 32 apart. On its own, outside a shell, it fills its container and is its own column: the same padding steps follow its own width, and its header starts 24 from its top. A 320px right rail (--bt-w-aside) sits beside them from 1040px of column beside a full sidebar and under them otherwise, and a section's title truncates with an ellipsis and shows in full as its title.

See it in the roastery demo's customers, orders, overview, payouts, roast plan, settings, stock, subscriptions and wholesale.

  • Page header

    The top of an app page, with its title, an optional value and the page's actions.

  • Sidebar

    An app's navigation down the side: a workspace switcher, items, titled sections, groups that fold, and the pages visited lately.

  • TopBar

    The bar over a dashboard's page: search at its start, icon actions and a round create button at its end.

  • Inspector

    An editor's screen parts: the header bar, the stage it works on and the inspector of settings beside it.