Page Header
The standard prototype page top: optional breadcrumbs OR an eyebrow (with a leading icon), a title (page-title role, or the oversized display role for a hero), and a right-aligned actions slot.
Preview
The default form: a page-title-scale heading with an actions slot. The container is neutral and transparent; color is the consumer’s job.
<PageHeader title="Requirements">
<EsaButton slot="actions" variant="primary" icon="plus">New requirement</EsaButton>
</PageHeader> There is no description slot
This component takes no lede, subtitle, or description. A page header carries the page’s name and its actions; a sentence explaining the page to its reader is text about the page rather than of it, and the page’s own data and structure are what say what it is.
Identifying facts go in meta, which takes labeled pairs rather than free text (see below). A figure the page exists to report is a stat, so put it in an esa-stat under the header, where it reads at stat scale.
Meta pairs
Each entry in meta is a { label, value } pair: the label is the field's name in the record, the value is what is stored there. Both parts are required.
That is why it is a prop and not a slot. A slot takes anything, and what it took was a row of facts glued together with separators, like SFO, Shoreline Protection Program, Beacon compliance platform
. A client, a program and a platform are three different kinds of thing, and nothing on that line says which is which. A pair cannot be written that way.
A period pair states the calendar it counts by. This domain runs fiscal years and water years alongside calendar ones, and a compliance figure means a different span under each.
<PageHeader
title="Quarterly Compliance Summary"
meta={[
{ label: 'Project', value: 'Delta Conveyance Project' },
{ label: 'Reporting period', value: 'Calendar Q2 2026' },
{ label: 'Lead agency', value: 'DWR' },
]}>
<EsaButton slot="actions" variant="ghost" appearance="outline" icon="download">Export</EsaButton>
</PageHeader> Eyebrow + display prominence (hero)
For a landing or hero, add an eyebrow (with an optional leading icon) and bump the title to the display role with prominence="display". The actions here are link-buttons — esa-button with an href.
<PageHeader
eyebrow="Annual Report"
icon="award"
prominence="display"
title="2026 in Review">
<EsaButton slot="actions" variant="primary" href="/report">Read the report</EsaButton>
<EsaButton slot="actions" variant="ghost" appearance="outline" href="/data">View the data</EsaButton>
</PageHeader> With breadcrumbs
Host an esa-breadcrumbs in the breadcrumbs slot for a record or nested page.
<PageHeader title="Permit #4471" eyebrow="Filed 12 days ago">
<nav slot="breadcrumbs">Permits / Active / #4471</nav>
<EsaButton slot="actions" variant="ghost" appearance="outline" icon="pencil">Edit</EsaButton>
</PageHeader> API
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | The page’s name, as it appears in navigation and breadcrumbs (required). Rendered as the <h1>. |
pairs | [{ label: 'Project', value: 'Delta Conveyance Project' }]. Renders | required | |
meta | EsaMetaPair[] | — | Identifying facts about the record this page shows, as {label, value} pairs. Renders as a definition list under the title: a wrapping row at width, one stack per pair below it. Both parts are required. A period pair states its calendar: "Calendar Q2 2026", not "Q2". |
eyebrow | string | — | The named thing this page sits inside — a section, a report, a person, a record id. A proper noun or a short label, never a sentence. |
icon | string | — | esa-icon registry name for the eyebrow’s leading glyph, e.g. "award" (eyebrow only). |
prominence | 'page-title' | 'display' | 'page-title' | Title scale: the page-title role (default) or the oversized display role for a hero. |
as | 'header' | 'div' | 'section' | 'header' | The host element tag. |
Slots
| Prop | Type | Default | Description |
|---|---|---|---|
breadcrumbs | slot | — | Above the title — host an esa-breadcrumbs here. Mutually exclusive in spirit with eyebrow. |
actions | slot | — | Right-aligned, top-aligned with the title block — buttons or links. |
Accessibility
- The title is the page
<h1>— there should be exactly one per page; keep heading order sensible below it. - The host defaults to a
<header>landmark; useas="div"when the header is nested inside another landmark. - The eyebrow icon is decorative (rendered via
esa-icon,aria-hidden) — the eyebrow text carries the meaning.
Theming surface
Every public token this component reads, extracted from its source at build time,
split by blast radius: the hooks that belong to this component,
then the shared and system tokens it consumes alongside everything else. The
Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
Type roles it names
This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.
| Role | Re-point | Reach |
|---|---|---|
.typography-body-md |
| +28 more componentsesa-app-shell, esa-breadcrumbs, esa-card, esa-checkbox, esa-checkbox-group, esa-collapsible, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-container, esa-danger-zone, esa-dialog, esa-dropdown-menu, esa-empty-state, esa-entity-search, esa-form-field, esa-header-nav, esa-input-tag, esa-nav-dropdown, esa-popover, esa-progress-bar, esa-radio-group, esa-select, esa-side-dialog, esa-snackbar-item, esa-switch-toggle, esa-tab-layout, esa-textarea |
.typography-display |
| only here |
.typography-heading-lg |
| only here |
.typography-label-md |
| +33 more componentsesa-app-bar, esa-breadcrumbs, esa-button-group, esa-button-toggle, esa-card, esa-checkbox-group, esa-color-picker, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-danger-zone, esa-date-picker, esa-dialog, esa-empty-state, esa-entity-search, esa-file-upload, esa-filter-container, esa-form-field, esa-header-nav, esa-input-tag, esa-link-column, esa-pillbox, esa-popover, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-side-dialog, esa-sidebar-nav, esa-stat, esa-text-field, esa-textarea, esa-tooltip |
Wired to this component
None. This component has no hooks of its own — it themes entirely through the shared tokens below, so every override here also moves its siblings.
Shared tokens it reads
Owned by a family or by the semantic layer. Still overridable — but scope the
override (esa-foo { --form-border-color: … }) unless you mean to move
everything that reads it. Reach says what else that is.
| Token | Reach | Resolves to |
|---|---|---|
--color-content-default | semantic every component | #202020 via --color-gray-12 |
--color-content-default-secondary | semantic every component | #646464 via --color-gray-11 |
--font-weight-medium | semantic every component | 500 via --typography-font-weight-medium → --font-weight-500 |
--typography-display-font-family | semantic every component | 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans |
--typography-display-font-size | semantic every component | clamp(2rem, 1.73rem + 1.35vw, 2.75rem) via --font-size-800 |
--typography-display-font-weight | semantic every component | 650 via --typography-font-weight-bold → --font-weight-650 |
--typography-heading-lg-font-family | semantic every component | 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans |
--typography-heading-lg-font-size | semantic every component | clamp(1.375rem, 1.2rem + 0.88vw, 1.875rem) via --font-size-600 |
--typography-heading-lg-font-weight | semantic every component | 550 via --typography-font-weight-semibold → --font-weight-550 |
--typography-label-md-font-size | semantic every component | clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200 |
--typography-label-sm-font-size | semantic every component | clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150 |
--line-height-normal | primitive every component | 1.6 |
--spacing-050 | primitive every component | 0.125rem |
--spacing-100 | primitive every component | 0.25rem |
--spacing-200 | primitive every component | 0.5rem |
--spacing-300 | primitive every component | 0.75rem |
--spacing-500 | primitive every component | 1.5rem |