GitHub

Header Nav

A batteries-included top app bar composed from the chrome bricks (esa-app-bar + esa-icon-link + esa-nav-dropdown): brand slot, data-driven primary nav with dropdowns, an actions slot, and a user menu. Disclosures are native details/summary, so it needs zero runtime JS. For full control, compose the bricks directly.

Stable .astro import EsaHeaderNav from '@esa/ecology/esa-header-nav.astro';

Preview

Open Reports or the avatar to expand the native disclosures.

ESA
JR
<EsaHeaderNav items={items} user={user}>
  <strong slot="brand">ESA</strong>
</EsaHeaderNav>

States

Brand only, no signed-in user — the avatar / user menu is omitted when user is absent.

<EsaHeaderNav items={items}>
  <strong slot="brand">Beacon</strong>
</EsaHeaderNav>

API

PropTypeDefaultDescription
items EsaHeaderNavItem[] [] Primary nav links. Each is { label; href?; icon?; children? }; an item with children renders a dropdown.
user EsaHeaderNavUser Optional signed-in user { name; email?; avatarUrl?; initials? }. When present, renders the avatar + user menu.

Slots

PropTypeDefaultDescription
brand slot Left-aligned logo / wordmark area.
actions slot Right-aligned controls placed before the user menu (e.g. search, notifications).

Nav dropdowns and the user menu are native details/summary disclosures (via esa-nav-dropdown) — no JavaScript required.

Accessibility

  • Each item with children renders an esa-nav-dropdown disclosure; plain items render an esa-icon-link.
  • Disclosures are native details/summary — keyboard toggling and the expanded state come for free.
  • The avatar button carries an aria-label naming the user (User menu for …).
  • Decorative chevrons and glyph SVGs are aria-hidden.

Theming surface

Every public token this component reads, extracted from its source at build time. 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.

TokenTierResolves to
--focus-ring-color component #65ba74 via --color-border-focus--color-grass-8
--focus-ring-width component 2px
--header-nav-avatar-bg component #46a758 via --color-primary--color-grass-9
--header-nav-avatar-size component 32px
--header-nav-avatar-text component #fcfcfc via --color-text-inverse--color-gray-1
--header-nav-border-color component #cecece via --color-border--color-gray-7
--header-nav-menu-item-color component #646464 via --color-text-secondary--color-gray-11
--color-border semantic #cecece via --color-gray-7
--color-danger-strong semantic #ce2c31 via --color-red-11
--color-surface semantic #fcfcfc via --color-gray-1
--color-surface-sunken semantic #f0f0f0 via --color-gray-3
--color-text-muted semantic #838383 via --color-gray-10
--color-text-primary semantic #202020 via --color-gray-12
--color-text-secondary semantic #646464 via --color-gray-11
--focus-ring-offset semantic 2px
--type-size-100 semantic clamp(0.625rem, 0.56rem + 0.32vw, 0.75rem)
--type-size-200 semantic clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem)
--font-weight-semibold primitive 550
--radius-100 primitive 0.25rem
--radius-200 primitive 0.5rem
--radius-full primitive 9999px
--shadow-200 primitive 0 4px 20px -4px rgba(0, 0, 0, 0.06)
--spacing-100 primitive 0.25rem
--spacing-150 primitive 0.375rem
--spacing-200 primitive 0.5rem
--spacing-300 primitive 0.75rem
--spacing-400 primitive 1rem
--spacing-500 primitive 1.5rem
--z-dropdown primitive 50