Layout
AppShell & TopBar
The frame every app rebuilds: a rail, a header, and exactly one scroll region between them.
import { AppShell, TopBar } from '@anomalia/ui'; Notes
- The shell is
100dvhandmainis the only scroll container. If the page scrolls instead, the rail scrolls away with it and a sticky header inside the content stops sticking — that single decision is most of what this component is. dvh, notvh: on iOS100vhis taller than the visible viewport while the browser chrome is up, so avhshell hangs its own bottom edge behind the address bar.- Bind
scrollElementand hand it toTopBar— the bar then watches the real scroller instead of the window, which is what makes its hairline appear at the right moment. - TopBar is frosted and keeps its hairline at all times, which is how the app has always drawn it: the rail, the bar and the content are three different surfaces, so the line separates real things from the first pixel.
divider="scroll"is there for a bar sitting directly on the content surface, where there genuinely is nothing to divide yet. - Its height is fixed rather than a minimum. Everything in a shell measures against the header — a drawer’s top edge, a sticky table head,
100dvh - header— so a bar that grows a pixel when a subtitle appears moves all of them. - The title is tracked tighter than the type ramp’s default and the eyebrow is smaller and airier than the caption default. At 56px those two lines have to read as chrome rather than as the first paragraph of the page.
- Below
compactAtthe page actions move into an overflow menu rather than being hidden. Hiding actions below a breakpoint hides the thing the page is for. - It measures its own width, not the viewport: the bar is the sidebar’s complement, so the window can be wide while the bar is not.
Examples
Frame
The shell is the only scroll region: the rail and the bar are fixed, `main` scrolls. Bind `scrollElement` and hand it to TopBar so the hairline appears when the content actually moves under it.
Scheduled
6
This week
Reach
184.2k +12% up
Pending
2
Need your copy
Monday
Behind the collection: three weeks in the studio, one afternoon of shooting.
Wednesday
Behind the collection: three weeks in the studio, one afternoon of shooting.
Friday
Behind the collection: three weeks in the studio, one afternoon of shooting.
Saturday
Behind the collection: three weeks in the studio, one afternoon of shooting.
API
AppShell
| Prop | Type | Default | Description |
|---|---|---|---|
sidebar / header / footer | Snippet | — | The rail, the fixed header, and a persistent bottom region. |
contentWidth | string | null | — | Caps the content column so prose does not run the width of a monitor. |
scrollElement | HTMLElement | null | — | Bindable. Hand it to TopBar. |
TopBar
| Prop | Type | Default | Description |
|---|---|---|---|
title / eyebrow | string | — | Where you are, and the section it belongs to. |
subtitle | string | — | One line under the title: a count, a state, the record being edited. |
divider | 'always' | 'scroll' | 'none' | 'always' | The hairline. `always` is the app’s look. |
glass / height | boolean / string | true / '56px' | Frosted background, and the fixed bar height the shell measures against. |
sticky | boolean | true | Pin to the top of the scroll container. |
compactAt | number | 720 | Width below which actions collapse into a menu. |
leading / actions / trailing | Snippet | — | Sidebar toggle, page actions (collapsible), and things that never collapse. |
Every component also forwards the native attributes of its root element, plus class, so anything not listed here still works.