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, anAppPage, and an optionalSidePanelat 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 width | Sidebar | SidePanel |
|---|---|---|
| 1280px and wider | In full | Docked at the shell's end |
| 1100px to 1280px | In full | Opens as a sheet |
| 640px to 1100px | An icon rail | Opens as a sheet |
| Under 640px | A sheet, opened from the top bar's menu button | Opens 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.
.app {
block-size: 100dvh;
}A dashboard with two pages in its sidebar:
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.
$40 of Halfmoon coffee
Format
Phone sheet
At phone width the inspector opens as a sheet; with open kept in useState, Done in the sheet closes it.
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
3The week so far, and what came in today.
This week
Orders
48
Bags roasted
212
Sales
$3,940
Today
Order 1053 · Blue Door Café
PaidOrder 1052 · Ines Vale
RoastingOrder 1051 · Pier 9 Kiosk
ShippedVariants 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.
Happy birthday, Rosa
Message
Props
AppShell
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
headerrequired | ReactNode | With layout="inspector": The bar at the top of the work area, such as a HeaderBar. | |
inspectorrequired | ReactNode | With layout="inspector": The settings beside the work area, such as an Inspector. On narrow screens it opens as a sheet. | |
inspectorLabelrequired | string | With layout="inspector": Names the inspector's sheet on narrow screens, such as "Gift card settings". | |
openInspectorLabel | string | "Edit" | With layout="inspector": The button that opens the sheet on narrow screens. Defaults to "Edit". |
open | boolean | With 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. | |
defaultOpen | boolean | false | Whether 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. | |
childrenrequired | ReactNode | With layout="inspector": The work area, such as a Stage. With layout="sidebar": The page, an AppPage. | |
sidebarrequired | ReactNode | With layout="sidebar": The app's navigation, a Sidebar. It becomes an icon rail under 1100px of shell and a sheet under 640. | |
topBar | ReactNode | With layout="sidebar": The bar over the page, a TopBar. | |
panel | ReactNode | With 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. | |
sidebarLabel | string | "Navigation" | With layout="sidebar": Names the sidebar's sheet on narrow screens. Defaults to "Navigation". |
skipLabel | string | "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
| Export | Description |
|---|---|
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(): AppShellState | Reads 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.
| Keys | What they do |
|---|---|
| Tab | In the sidebar layout, reaches Skip to content first; pressing it moves focus past the sidebar and the top bar to the page |
| Enter or Space | Opens 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+Tab | Moves through an open sheet's controls without leaving it |
| Enter | Follows a link in the sidebar's sheet and closes the sheet |
| Escape | Closes 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.
Related
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 ordersOrders
Order 1051, roasting
Order 1053, paid
Subscription
A bag of the house blend every two weeks.
Orders page
A contained page: its header, then sections 32px apart, the first with an action at the end of its heading.
Orders
1,284Needs 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 ordersOrders
The last order shipped on Tuesday.
Notes
Prefers a lighter roast.
Props
AppPage
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
header | ReactNode | The page's header, a PageHeader, with its tabs and quick actions inside it. | |
aside | ReactNode | A 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. | |
asideLabel | string | "Details" | Names the right rail's landmark, such as "Customer details". Defaults to "Details". |
fill | boolean | false | Gives 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. |
footer | ReactNode | A 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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | The section's heading, an h2 in title-md, which names it. | |
description | ReactNode | One line under the heading saying what the section is for, in body-sm and text-secondary. | |
actions | ReactNode | Actions at the end of the heading's row, such as a small "See all" button. |
It also takes Omit<MotionElementProps<"section">, "title">.
Also exported
| Export | Description |
|---|---|
AppPageContext: Context<AppPageState | null> | The state an AppPage shares, or null outside one. |
useAppPage(): AppPageState | null | Reads 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.
Related
See it in the roastery demo's customers, orders, overview, payouts, roast plan, settings, stock, subscriptions and wholesale.
Related
- 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.