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
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Page Header before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify page-headerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/page-headerRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/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), then the tabs row (tabs), followed by any children. 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: thebackRenderelement (your framework link) or an<a>whenbackHrefis set, each with aChevronLeft; a button whenonBackis set. Omitted when none 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-meta— the muted metadata row under the description, a<div>that can hold controls. Omitted when nometa.page-header-actions— the right-aligned slot holdingactionsthensecondaryMenu. Omitted when both are empty.page-header-tabs— the row holdingtabs, 24px under the title and, when it ends a header placed directly in anAppShellPage, 12px above the page below (the same rhythm as aListToolbar's tabs row, so every page reads title → tabs → content alike). Omitted when 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()} />Framework link for back
backHref renders a plain <a>, which reloads the page in a client-routed app. Pass your router's
link element as backRender instead — it receives the back affordance's classes, aria-label,
data-slot and chevron, and navigates client-side. backRender wins over backHref; onBack
wins over both.
import Link from "next/link";
<PageHeader
backRender={<Link href="/products" />}
backLabel="Back to products"
title={product.name}
/>;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" />Wrapping title
titleLines sets how many lines the title may take before it clips — 2 clamps to two lines —
and "none" lets it wrap freely with no truncation, for record pages whose name is the content.
An unbroken token — a 170-character file name, a pasted URL — breaks anywhere rather than pushing
the page sideways, and inside a clamp it breaks too, so the ellipsis still lands.
Q3 Platform Reliability & Performance Engineering Initiative Retrospective
View tabs
On a list page, the scope tabs — whose records it shows ("My tasks | Created by me | Team") — do
not go in tabs: they go in the ListToolbar's tabs, on the
toolbar row (tabs left, tools right), where they fold to a dropdown when tight. A list page passes
no actions either: its main button is the toolbar's action. The header keeps the title,
description, meta and ⓘ (see Lists and tables).
tabs is for a record page's content tabs and a settings page's sub-page route tabs
(RouteTabs overflow="menu", which folds on a phone instead of clipping).
Tasks
<PageHeader
title="Tasks"
actions={<Button>New task</Button>}
tabs={
<Tabs value={scope} onValueChange={setScope}>
<TabsList aria-label="Tasks to show">
<TabsTrigger value="mine">My tasks</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
</TabsList>
</Tabs>
}
/>
<ListToolbar
aria-label="Task toolbar"
search={{ value: query, onValueChange: setQuery, placeholder: "Search tasks" }}
filterRow={<FilterRow facets={facets} />}
layout={{ value: view, options: ["list", "board"], onValueChange: setView }}
/>Metadata with controls
description is a paragraph, so a button or select inside it is invalid HTML. Put status badges,
owners, dates and inline pickers in meta instead: a muted, wrapping <div> row under the title.
Onboarding redesign
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 icon-only Button 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={
<Button size="icon" variant="ghost" aria-label="More actions">
<MoreVertical />
</Button>
}
/>
<DropdownMenuContent>{/* … */}</DropdownMenuContent>
</DropdownMenu>
}
/>Section headings
SectionHeading titles an in-page section below the PageHeader, so a page never hand-rolls
<h2 className="text-base font-medium">. size="md" (16px, the default) titles a page section,
size="sm" (14px) a card or panel section, and variant="eyebrow" is the quiet muted 12px label
above a group. actions sits at the end of the same row; as sets the outline level.
import { SectionHeading } from "@/components/ui/page-header";
<SectionHeading actions={<Button variant="ghost" size="sm">View all</Button>}>
Recent activity
</SectionHeading>
<SectionHeading size="sm" as="h3">Contacts</SectionHeading>
<SectionHeading variant="eyebrow" as="h3">Pinned</SectionHeading>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. |
backRender | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Renders the back link through your framework's link element — pass an element such as
<Link href="/products" /> (Next.js) and it receives the back affordance's classes,
aria-label, data-slot and chevron, so navigation stays client-side instead of a full
page load. backHref is the shorthand for backRender={<a href={backHref} />}. Ignored when
onBack is set; wins over backHref. |
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. |
meta | React.ReactNode | — | Metadata under the title — status badges, owners, dates, or inline pickers. Unlike
description (a <p>), this is a <div> row, so it can hold buttons, selects and other
interactive controls without invalid HTML. Muted, text-sm, wrapping. |
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
icon Button trigger) — kept as a slot so the header stays presentational. |
tabs | React.ReactNode | — | The page's scope tabs, on their own row 24px under the title (12px above the page below in an
AppShellPage: the ListToolbar tabs rhythm) — a default (pill) Tabs
such as "My tasks | Created by me | Team" that switches whose records a list page shows.
Tabs that change content in place belong here, above the list's toolbar, not inside it.
How the records are laid out ("List | Board") is not a tab: it is the ListToolbar's Layout
tool. |
titleLines | number | "none" | 1 | How many lines the title may take before it clips with an ellipsis (the full title then
shows in a Tooltip). "none" lets a long title wrap freely, with no truncation at all —
for record pages whose name is the content. |
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-meta" | "page-header-tabs" | "page-header-title" |
data-title-lines | mirrors a prop or state value |
SectionHeading
| Prop | Type | Default | Description |
|---|---|---|---|
actions | React.ReactNode | — | Content at the end of the row — a "View all" link, a small action. |
as | "h2" | "h3" | "h4" | "h2" | The heading element, for the document outline. |
size | "md" | "sm" | "md" | md titles a page section (16px, medium); sm titles a card or panel section (14px,
medium). |
variant | "default" | "eyebrow" | "default" | eyebrow is the quiet label above a group — 12px, muted, medium — instead of a title. |
Data attributes and CSS variables on SectionHeading
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "section-heading" | "section-heading-row" |
data-variant | mirrors a prop or state value |
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>in the page-heading face (font-heading text-2xl font-semibold) — 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 (
backRenderorbackHref) 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 |