App Topbar
Breadcrumb + title + status + actions
What it is
The single header that runs across the top of a shell — one component with two lead configurations. In document context it opens with a breadcrumb and a privacy chip; in editor context it opens with zoom controls and undo/redo history. Both configurations share the same title, saved-state, and trailing-actions slots, and a chat title bar is simply this with fewer of those slots filled.
Why it matters
LTX Studio, CapCut, Canva, Spline, Freepik, and Descript all put a single persistent bar across the top of the workspace, but what leads it depends entirely on what the surface below it is: a document surface needs to say where you are (breadcrumb) and who can see it (privacy); a canvas or timeline surface needs to say how you're viewing it (zoom) and whether you can back out of the last change (history). Modeling that as one component with a `context` switch — rather than two unrelated headers — keeps title, saved-state, and actions from drifting out of sync between the two.
Observed in: LTX Studio · CapCut · Canva · Spline · Freepik · Descript
Anatomy
- 1
app-topbarRoot<header>landmark spanning the shell width. - 2
app-topbar-leadingContext-specific lead group plus the privacy chip. - 3
app-topbar-breadcrumbDocument context: path to the current document. - 4
app-topbar-zoomEditor context: zoom-out / level / zoom-in button group. - 5
app-topbar-historyEditor context: undo / redo button group. - 6
app-topbar-titleDocument or scene name — shown when there's no breadcrumb, or alongside zoom/history. - 7
app-topbar-privacyIcon + text status chip. Available in either context. - 8
app-topbar-trailingRight-aligned group holding saved-state and actions. - 9
app-topbar-savedSaved-state as plain readable text, e.g. "Last saved 5 days ago". - 10
app-topbar-actionsConsumer-supplied trailing controls (share, export, presence, …).
How to use it
Reach for it as the one top bar for a shell and pick `context` based on what's underneath it: `document` for anything read top-to-bottom (docs, pages, notes) where breadcrumb + privacy matter most; `editor` for anything manipulated on a canvas or timeline (design files, video, 3D) where zoom + history matter most. Pass `savedLabel` as a finished sentence, not a timestamp to format — the component never guesses relative time for you. For a chat surface, use the same component with just `title` and `actions` filled in; don't reach for a separate chat-header component.
Do
Render saved-state as a finished sentence — "Last saved 5 days ago" answers the question a status glyph only raises.
Last saved 5 days agoLet editor context lead with zoom + history instead of a breadcrumb — it's the same component, just the other configuration.
Untitled scenePair the privacy chip's icon with a visible label — the icon alone is decoration, the text is the signal.
Private
Don't
Don't render saved-state as an icon-only indicator — a bare cloud glyph forces the user to hover or guess instead of just reading.
Q3 RoadmapDon't signal privacy with a bare coloured dot — it disappears for colorblind users and screen readers alike.
Q3 Roadmap
Accessibility
- Keyboard
- Document context has one tab stop per breadcrumb crumb that carries an `href`, and none otherwise. The final crumb is deliberately not a link, a crumb without `href` renders as plain text, and the title, privacy chip and saved-state line are never focusable.
- Editor context is always four tab stops before your `actions` — Zoom out, Zoom in, Undo, Redo — and they render whether or not you passed the matching handler. An editor topbar with no `onUndo` still presents an enabled, focusable Undo button that does nothing.
- `canUndo` and `canRedo` default to `true`, so `disabled` only appears when you say so. Omitting them is not the same as having no history.
- The button groups have no roving tabindex and no arrow-key navigation — they are `role="group"` wrappers, so each control is its own stop.
- No shortcuts are bound. The zoom and history controls respond to Space and Enter as buttons and to nothing else; Cmd+Z is your app's job.
- Screen reader
- The root is a `<header>`, which is a banner landmark only when it is not inside another landmark. Nested in a shell's `<main>` it is a generic container instead, and either way it carries no `aria-label` — two topbars on one page are indistinguishable.
- The breadcrumb is a `<nav aria-label="breadcrumb">`; the final crumb announces as a disabled link with `aria-current="page"`, and each separator is `role="presentation"` so it is not read.
- In document context with a `breadcrumb`, the `title` prop renders nowhere at all — the last crumb's label is what appears. If the two ever differ, the name a screen reader hears is the crumb's, not the title's.
- Zoom and history are icon-only buttons named entirely by `aria-label` — "Zoom out", "Zoom in", "Undo", "Redo". Those strings are hard-coded and not localisable through this component.
- The zoom level is rendered as ordinary text between the two buttons, with no `aria-live` and no relationship to either. Pressing Zoom in changes the number silently, so a screen-reader user gets no feedback that the press did anything.
- The saved-state line is the same shape: plain text, no live region. "Last saved 5 days ago" becoming "Saving…" is a change nobody is told about.
- The privacy chip announces as its label text; its optional glyph is wrapped `aria-hidden`, which is what makes the visible word rather than the icon the actual signal.
- Focus
- Switching `context` swaps the entire leading group. If focus was on Zoom in when the surface flips to document context, the button unmounts and focus falls to `<body>` — put the switch behind a navigation rather than a toggle in the bar itself.
- Disabling Undo when history empties removes it from the tab order under whoever was standing on it, dropping focus to `<body>` mid-edit.
- Every control inherits the shared button's `focus-visible` ring; the topbar adds no focus styling of its own.
Watch out for
- Building a second header component for editor surfaces instead of switching
context— the whole point of B7 is that document and editor bars are one component, so title, saved-state, and actions stay wired the same way in both. - Formatting
savedLabelas a raw ISO timestamp or Date and letting the component render it verbatim — it renders exactly the string you pass, so compute the relative phrasing ("5 days ago") before handing it over. - Disabling undo/redo by simply omitting
onUndo/onRedo— the buttons still render enabled by default; passcanUndo/canRedoexplicitly so the disabled state matches real history state.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/app-topbar.jsonRequires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.