Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
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-header

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/page-header

Run 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:

FavoriteStar — data-slot="page-header-favorite"
PageHeader — 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"
SectionHeading — data-slot="section-heading" | "section-heading-row"
<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 when PageHeader sits 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 no breadcrumb.
  • page-header-back — the back affordance: the backRender element (your framework link) or an <a> when backHref is set, each with a ChevronLeft; a button when onBack is set. Omitted when none is set.
  • page-header-title — the page <h1>. The title text itself routes through TruncatedText (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 — TruncatedText composes inside it, never replaces it.
  • page-header-favorite — the star toggle. Sets aria-pressed and data-active. Omitted when no favorite.
  • page-header-description — the muted description under the title. Omitted when no description.
  • page-header-meta — the muted metadata row under the description, a <div> that can hold controls. Omitted when no meta.
  • page-header-actions — the right-aligned slot holding actions then secondaryMenu. Omitted when both are empty.
  • page-header-tabs — the row holding tabs, 24px under the title and, when it ends a header placed directly in an AppShellPage, 12px above the page below (the same rhythm as a ListToolbar'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:

API Keys

Manage keys for this workspace.

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()} />

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.

Linen shirt

The back link navigates client-side through the router's own Link.

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

In progressUpdated 2 hours ago

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

PropTypeDefaultDescription
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.
actionsReact.ReactNode—Right-aligned action slot — typically one or more Buttons. Rendered on the title row, opposite the title block.
backHrefstring—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.
backLabelstring'Go back'Accessible name for the back button.
backRenderReact.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.
breadcrumbReact.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.
descriptionReact.ReactNode—Optional supporting copy rendered under the title (muted).
favoritePageHeaderFavorite—Optional favorite-star toggle rendered after the title. Omit to hide it.
metaReact.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.
secondaryMenuReact.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.
tabsReact.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.
titleLinesnumber | "none"1How 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

AttributeValues
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-linesmirrors a prop or state value

SectionHeading

PropTypeDefaultDescription
actionsReact.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

AttributeValues
data-sizemirrors a prop or state value
data-slot"section-heading" | "section-heading-row"
data-variantmirrors a prop or state value

PageHeaderFavorite

PropTypeDefaultDescription
activeboolean—Controlled active (starred) state. When provided, the host owns the state and must update it from onToggle. Omit to run uncontrolled.
defaultActivebooleanfalseInitial active state when uncontrolled (no active prop).
disabledbooleanfalseDisables the toggle and removes it from the tab order.
labelstring'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 when PageHeader sits 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 via Tab and a focus-triggered tooltip — not hover-only.
  • The back affordance is a real link (backRender or backHref) or button (onBack) with an accessible backLabel (default "Go back"), and shows a visible :focus-visible ring.
  • The favorite star is a toggle button: it carries aria-pressed reflecting the starred state and a label (default "Favorite"); the visual fill is decorative.
  • All interactive parts are keyboard reachable and operable; the visual order matches the DOM order.
KeyAction
TabMove focus through the back button, favorite star, and actions.
EnterFollow the back link, or activate the focused button.
SpaceActivate the focused button (back / favorite / action).
ContractStates tested
Behaviourdefault, active, disabled, pressed
Accessibilitydisabled, labeled, pressed, semantic-html
Visualdefault, hover, active

Do / Don't

Do
Keep one PageHeader (one h1) per page, pass your router's Link as backRender, and reserve onBack for app-local behavior.
Don't
Stack multiple PageHeaders or nest an h1 inside another heading — and don't hardcode navigation inside the header.

On this page