Skip to content

Layout

AppShell & TopBar

The frame every app rebuilds: a rail, a header, and exactly one scroll region between them.

AppShell TopBar
import { AppShell, TopBar } from '@anomalia/ui';

Notes

  • The shell is 100dvh and main is 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, not vh: on iOS 100vh is taller than the visible viewport while the browser chrome is up, so a vh shell hangs its own bottom edge behind the address bar.
  • Bind scrollElement and hand it to TopBar — 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 compactAt the 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.

Lindaria

Content

Andrea Buttarelli Marta Rossi

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

PropTypeDefaultDescription
sidebar / header / footerSnippetThe rail, the fixed header, and a persistent bottom region.
contentWidthstring | nullCaps the content column so prose does not run the width of a monitor.
scrollElementHTMLElement | nullBindable. Hand it to TopBar.

TopBar

PropTypeDefaultDescription
title / eyebrowstringWhere you are, and the section it belongs to.
subtitlestringOne 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 / heightboolean / stringtrue / '56px'Frosted background, and the fixed bar height the shell measures against.
stickybooleantruePin to the top of the scroll container.
compactAtnumber720Width below which actions collapse into a menu.
leading / actions / trailingSnippetSidebar 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.