GitHub

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.

Stable .astro import EsaPageHeader from '@esa/ecology/esa-page-header.astro';

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.

Requirements

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

Quarterly Compliance Summary

Project
Delta Conveyance Project
Reporting period
Calendar Q2 2026
Lead agency
DWR
<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.

Annual Report

2026 in Review

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

Host an esa-breadcrumbs in the breadcrumbs slot for a record or nested page.

Filed 12 days ago

Permit #4471

<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

PropTypeDefaultDescription
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

PropTypeDefaultDescription
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; use as="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.

RoleRe-pointReach
.typography-body-md
  • --typography-body-md-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-md-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-body-md-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-md-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-md-line-height 1.6 via --line-height-normal
+28 more components esa-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
  • --typography-display-font-family 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans
  • --typography-display-font-size clamp(2rem, 1.73rem + 1.35vw, 2.75rem) via --font-size-800
  • --typography-display-font-weight 650 via --typography-font-weight-bold → --font-weight-650
  • --typography-display-letter-spacing -0.01em via --letter-spacing-tight
  • --typography-display-line-height 1.3 via --line-height-tight
only here
.typography-heading-lg
  • --typography-heading-lg-font-family 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans
  • --typography-heading-lg-font-size clamp(1.375rem, 1.2rem + 0.88vw, 1.875rem) via --font-size-600
  • --typography-heading-lg-font-weight 550 via --typography-font-weight-semibold → --font-weight-550
  • --typography-heading-lg-letter-spacing -0.01em via --letter-spacing-tight
  • --typography-heading-lg-line-height 1.3 via --line-height-tight
only here
.typography-label-md
  • --typography-label-md-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-md-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-label-md-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-md-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-md-line-height 1.6 via --line-height-normal
+33 more components esa-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