Page Header
The standardized header at the top of a page — back button, breadcrumb trail, title, description, actions, secondary menu, and a favorite star.
- Status
- Since
0.1.0- Accessibility pattern
- banner landmark
Last updated
Spaces
Organize work into shared spaces.
Install
Add Page Header from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/page-headerThe same command installs the registry items it composes: @vegastack/button, @vegastack/icon-button, @vegastack/truncated-text.
Usage
import { PageHeader } from "@/components/ui/page-header";
import { Button } from "@/components/ui/button";
<PageHeader
title="Spaces"
description="Organize work into shared spaces."
actions={<Button>New space</Button>}
/>;PageHeader is presentational — it owns layout, not behavior. You pass navigation
(backHref / onBack), the breadcrumb node, right-side actions and secondaryMenu
slots, and a favorite config; the host owns routing and persistence.
The registry component is client-rendered because it includes the optional favorite toggle and
imperative onBack button path. Prefer backHref for URL-backed navigation; use onBack only for
app-local behavior such as closing a picker or returning to a non-URL state.
Anatomy
PageHeader lays out, top to bottom, an optional breadcrumb row, then a title row split into a
left title block (back button, <h1>, favorite star, description) and a right actions block
(actions + secondary menu), followed by any children (tabs, search bars). Every region exposes a
data-slot for styling and testing:
<PageHeader
backHref="/settings" // ← page-header-back (<a>, ChevronLeft)
breadcrumb={<Breadcrumb>…</Breadcrumb>} // ← page-header-breadcrumb
title="API Keys" // ← page-header-title (<h1>)
description="Manage keys." // ← page-header-description
favorite={{ defaultActive: true }} // ← page-header-favorite (Star toggle)
actions={<Button>Create key</Button>} // ← page-header-actions
secondaryMenu={<DropdownMenu>…</DropdownMenu>}
>
{/* children render below the header row */}
</PageHeader>page-header— the root container (<header>, vertical stack). A banner landmark whenPageHeadersits at the top of the page — not nested inside<article>/<aside>/<main>/<nav>/<section>.page-header-breadcrumb— wraps the breadcrumb node above the title row. Omitted when nobreadcrumb.page-header-back— the back affordance (<a>with aChevronLeftwhenbackHrefis set, a button whenonBackis set). Omitted when neither is set.page-header-title— the page<h1>. The title text itself routes throughTruncatedText(data-slot="truncated-text", nested inside the<h1>), so an overlong tenant/workspace title clips with an ellipsis instead of overflowing the header row, and reveals in full via a tooltip on hover/focus. The<h1>stays the accessible heading —TruncatedTextcomposes inside it, never replaces it.page-header-favorite— the star toggle. Setsaria-pressedanddata-active. Omitted when nofavorite.page-header-description— the muted description under the title. Omitted when nodescription.page-header-actions— the right-aligned slot holdingactionsthensecondaryMenu. Omitted when both are empty.
Examples
Minimal
The only required prop is title. With nothing else, PageHeader renders just the <h1> —
every other slot (back button, breadcrumb, description, favorite, actions) is opt-in:
Profile
<PageHeader title="Profile" />With breadcrumb, back button, and actions
A back button (rendered as a link via backHref), a full breadcrumb trail, a description, and two
actions on the right:
Back navigation
Pass backHref for URL-backed navigation (renders an <a>) or onBack for an app-specific
imperative action (renders a <button>). The two render different elements; onBack takes
precedence when both are set. The preview below uses onBack, so the back affordance is a
real <button>:
Select plan
Choose a plan to continue. The back button closes the picker.
// Link — renders an <a>
<PageHeader title="Select plan" backHref="/billing" />
// Imperative app action — renders a <button>
<PageHeader title="Select plan" onBack={() => closePlanPicker()} />Long title
An overlong tenant or workspace title truncates with an ellipsis instead of overflowing the
header row. The full title reveals via a tooltip on hover or keyboard focus — a tap-to-toggle
disclosure on devices that can't hover (see TruncatedText):
Q3 Platform Reliability & Performance Engineering Initiative Retrospective
An overlong tenant or workspace title truncates instead of overflowing, and reveals in full on hover or focus (tap on touch).
<PageHeader title="Q3 Platform Reliability & Performance Engineering Initiative Retrospective" />Favorite star
The star is controlled when you pass active, or uncontrolled when you pass defaultActive. It
reports the next state through onToggle. Pass disabled to remove it from the tab order.
The active star fills with neutral ink, not warning yellow. The status hues are rationed to actual status, and a favourite is a user's own mark rather than a caution — the fill is what carries the on/off read, so the colour was never doing that work.
The preview shows a controlled star (click to toggle) above a disabled one:
Q3 Roadmap
Controlled star — currently not starred.
Archived doc
Disabled star — non-interactive and out of the tab order.
// Uncontrolled — seeds initial state, host persists on toggle
<PageHeader title="Doc" favorite={{ defaultActive: true, onToggle: (next) => persist(next) }} />
// Controlled — host owns the state
<PageHeader title="Doc" favorite={{ active: isStarred, onToggle: setStarred }} />
// Disabled — non-interactive, out of the tab order
<PageHeader title="Doc" favorite={{ defaultActive: true, disabled: true }} />Secondary menu
Compose your own overflow menu (e.g. a DropdownMenu with an IconButton trigger) and pass it via
secondaryMenu — it renders to the right of actions:
Spaces
The overflow menu renders to the right of the actions.
<PageHeader
title="Spaces"
actions={<Button>New space</Button>}
secondaryMenu={
<DropdownMenu>
<DropdownMenuTrigger
render={
<IconButton aria-label="More actions">
<MoreVertical />
</IconButton>
}
/>
<DropdownMenuContent>{/* … */}</DropdownMenuContent>
</DropdownMenu>
}
/>API Reference
PageHeader
| Prop | Type | Default | Description |
|---|---|---|---|
title* | React.ReactNode | — | The page title — rendered as the <h1>. Accepts a string or rich nodes.
Truncated via TruncatedText when it overflows the title row, revealing
the full title on hover/focus (tap on touch) — see the component doc. |
actions | React.ReactNode | — | Right-aligned action slot — typically one or more Buttons. Rendered on the
title row, opposite the title block. |
backHref | string | — | Renders a back button (ChevronLeft) before the title as a link to this
href. Use for declarative navigation; prefer over onBack when you have a
URL. Ignored when onBack is also set. |
backLabel | string | 'Go back' | Accessible name for the back button. |
breadcrumb | React.ReactNode | — | Optional breadcrumb trail (or section name) rendered above the title row.
Pass a Breadcrumb element or any node — the header is presentational and
does not build the trail for you. |
description | React.ReactNode | — | Optional supporting copy rendered under the title (muted). |
favorite | PageHeaderFavorite | — | Optional favorite-star toggle rendered after the title. Omit to hide it. |
onBack | (() => void) | — | Renders a back button (ChevronLeft) before the title that calls this
handler. Use for app-local imperative behavior, such as closing a picker
or returning to the previous in-app state. Prefer backHref for URL-backed
navigation. |
secondaryMenu | React.ReactNode | — | Optional overflow / secondary menu slot, rendered after actions on the
right. Compose your own menu trigger (e.g. a DropdownMenu with an
IconButton trigger) — kept as a slot so the header stays presentational. |
Data attributes and CSS variables on PageHeader
| Attribute | Values |
|---|---|
data-slot | "page-header" | "page-header-actions" | "page-header-back" | "page-header-breadcrumb" | "page-header-description" | "page-header-title" |
PageHeaderFavorite
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | — | Controlled active (starred) state. When provided, the host owns the state
and must update it from onToggle. Omit to run uncontrolled. |
defaultActive | boolean | false | Initial active state when uncontrolled (no active prop). |
disabled | boolean | false | Disables the toggle and removes it from the tab order. |
label | string | 'Favorite' | Accessible name for the toggle. The current state is announced via
aria-pressed, so pass the action label only. |
onToggle | ((active: boolean) => void) | — | Called with the next active state whenever the star is toggled. |
FavoriteStar
FavoriteStar renders the controlled star toggle used by PageHeader's favorite slot. Prefer the
favorite prop for ordinary page headers; export-level composition is available when the same toggle
must be placed elsewhere.
Accessibility
- The root renders
<header>, a banner landmark whenPageHeadersits at the top of the page (outside<article>/<aside>/<main>/<nav>/<section>), so assistive tech can jump straight to it. - The title renders as a single
<h1>— one per page — so assistive tech announces the page's primary heading. Pass the breadcrumb as a labelled<nav aria-label="breadcrumb">. - An overlong title is keyboard-focusable when clipped (
TruncatedText's overflow measurement), so the full title is reachable viaTaband a focus-triggered tooltip — not hover-only. - The back affordance is a real link (
backHref) or button (onBack) with an accessiblebackLabel(default"Go back"), and shows a visible:focus-visiblering. - The favorite star is a toggle button: it carries
aria-pressedreflecting the starred state and alabel(default"Favorite"); the visual fill is decorative. - All interactive parts are keyboard reachable and operable; the visual order matches the DOM order.
| Key | Action |
|---|---|
| Tab | Move focus through the back button, favorite star, and actions. |
| Enter | Follow the back link, or activate the focused button. |
| Space | Activate the focused button (back / favorite / action). |
| Contract | States tested |
|---|---|
| Behaviour | default, active, disabled, pressed |
| Accessibility | disabled, labeled, pressed, semantic-html |
| Visual | default, hover, active |
Do / Don't
Sidebar
A collapsible app navigation rail — header / content / footer, labelled groups, menu items with active state, and an expand/collapse trigger.
Board
Kanban columns over the drag-reorder seam — pointer drag, keyboard move mode, a lossless per-card Move menu, server-refusable moves, collapsed lanes.