Overview
The header system is composed of several coordinating components. All components are CORE-OWNED unless noted.
Who this is for
- Implementors configuring site header layout, logo, and CTA buttons
- Developers modifying header component behavior or adding new header controls
| Component | Location | Purpose |
|---|---|---|
Header.astro | src/core/components/nav/ | Site header shell — composes all controls |
MainNav.astro | src/core/components/nav/ | Desktop primary navigation with submenu support |
MobileMenu.astro | src/core/components/nav/ | Mobile navigation panel using Drawer |
HamburgerButton.astro | src/core/components/nav/ | Accessible mobile menu toggle |
SiteLogo.astro | src/core/components/nav/ | Config-driven logo with reactive theme switching |
ThemeSwitcher.astro | src/core/components/nav/ | Config-driven wrapper for theme variants |
ThemeIconSwitcher.astro | src/core/components/nav/ | Icon-cycling theme button |
ThemeModeSwitcher.astro | src/core/components/nav/ | ListBox-based theme select |
SearchTrigger.astro | src/core/components/search/ | Forward-compatible search entry point |
Drawer.astro | src/core/components/primitives/ | Reusable slide-in overlay primitive |
Header
Header.astro renders the site navigation bar. The navigation landmark is provided by the <nav> element inside MainNav. The banner landmark lives on the subheader slot wrapper in PageGridLayout, not on this component.
Composition:
SkipLink— first childSiteLogo— site identityMainNav— desktop navigation (hidden on mobile)SearchTrigger— search icon button (hidden on mobile, lives in mobile panel)ThemeSwitcher— theme controls (hidden on mobile, lives in mobile panel)CTA Button— optional, configured viasiteConfig.header.ctaHamburgerButton— mobile menu toggle (hidden on desktop)MobileMenu— mobile navigation panel
Configuration:
siteConfig.header.stickyHeader—'none' | 'sticky' | 'scroll-up'(default:'scroll-up'):'none'— default block flow; header scrolls with the page'sticky'—position: sticky; top: 0; header sticks when it reaches the viewport top'scroll-up'—position: fixed; top: 0; header hides when the user scrolls down past 50px and reappears when they scroll up. A vanilla JS initializer sets--header-heighton<html>so the grid row maintains its height. CSS transition respectsprefers-reduced-motion.
siteConfig.header.navPosition—'left' | 'center' | 'right', controls MainNav placementsiteConfig.header.mobileMenuPosition—'left' | 'right' | 'full-width', controls which side the mobile drawer slides in from (default:'right')siteConfig.header.cta— optional CTA button ({ label, href, variant? })
--header-height CSS variable:
When stickyHeader: 'scroll-up' is active, Header.astro sets --header-height on <html> from the rendered header height on mount. This variable is consumed by PageGridLayout’s [data-area="header"] grid item to maintain its height while the fixed header overlays it. It is also available for use in other layout contexts that need to account for the header offset.
PageGridLayout integration:
PageGridLayout renders <Header /> by default. Pages can:
- Default: Header renders automatically
- Override: Pass a component to the
headernamed slot - Suppress: Pass
showHeader={false}
MainNav
MainNav.astro renders the desktop primary navigation from menuConfig.mainNav.
- Top-level items with
childrenrender a submenu viaDropdown.astro aria-current="page"on the active top-level itemdata-activeon parent items when a child route is active- Submenu opens on click/Enter/Space; closes on ESC with focus return
- Arrow key navigation within submenus
Dropdown.astroconsumes the nav dropdown semantic surface for its panel (--st-color-nav-dropdown-bg/--st-color-nav-dropdown-border)
MobileMenu & HamburgerButton
MobileMenu.astro uses Drawer.astro. The drawer position is controlled by siteConfig.header.mobileMenuPosition ('left' | 'right' | 'full-width', default 'right'). Contains:
- Navigation links (same data as MainNav, vertical layout)
SearchTrigger(when search is enabled)ThemeSwitcher(when theme switcher is not'none')Drawer.astrouses the mobile navigation semantic surface (--st-color-nav-mobile-bg) and the shared overlay scrim token for the backdrop
The CTA button is not duplicated in the mobile panel — it stays in the main header bar.
HamburgerButton.astro toggles the mobile menu. It renders a <button> with the bars.svg icon, aria-expanded, and aria-controls pointing to the mobile panel ID.
The shared state ({ mobileOpen: false }) lives on Header.astro’s root wrapper, managed by a vanilla TypeScript controller.
SiteLogo
SiteLogo.astro accepts a location: 'header' | 'footer' prop and renders the logo from siteConfig.branding.
Fallback chain:
logoMainpresent — renders<img>with the URL resolved fromlogoUsageMatrix[location][currentTheme]logoMainabsent — renderslogoText(orsiteNameiflogoTextis unset) as a<span>
Props:
location—'header' | 'footer'(required)variant?— explicitLogoVariantoverride, takes precedence overlogoUsageMatrix(REQ-00157)alt?— override alt text; defaults tologoText, thensiteConfig.siteName(REQ-00158)
Reactive theme switching: Vanilla JS maintains a currentTheme variable initialised from data-theme on mount. When theme-changed fires, currentTheme is updated from event.detail.theme, triggering an immediate <img src> swap without a page reload.
Logo asset pattern: logoMain and logoAlt are resolved URL strings imported in site.ts via Vite’s ?url suffix. See Site Configuration — Branding & Logos for the full setup pattern.
ThemeSwitcher
Three components work together:
ThemeSwitcher.astro— readssiteConfig.theme.themeSwitcher('icon' | 'select' | 'none') and renders the appropriate variantThemeIconSwitcher.astro— cycles system → light → dark → system on click (REQ-00162)ThemeModeSwitcher.astro— ListBox-based dropdown select
Shared storage contract: Both variants use setTheme(mode) and getTheme() from src/lib/theme.ts. The theme-changed custom event on window synchronizes all instances.
Multi-instance sync: Desktop and mobile instances listen for theme-changed events and update their displayed state without polling.
SearchTrigger
SearchTrigger.astro renders an icon button with data-search-trigger as a forward-compatible hook.
Behavior modes:
siteConfig.header.search.behavior = 'modal'— renders a<button>; clicking does nothing until the search modal is implementedsiteConfig.header.search.behavior = 'page'— renders an<a>linking tositeConfig.header.search.hrefsiteConfig.header.search.enabled = false— suppresses the trigger entirely
Drawer
Drawer.astro is a reusable slide-in overlay primitive in src/core/components/primitives/.
Props:
id— required, anchorsaria-controlsposition?—'left' | 'right' | 'full-width'(default:'right')binding?— variable name controlling open/close (default:'open')label?—aria-labelfor the dialog region
Features:
- Focus trap via vanilla JS keyboard handling
- Backdrop overlay click closes the panel
- ESC closes the panel
- Scroll lock on
<body>while open - CSS transitions with
prefers-reduced-motionsupport
HeaderConfig Schema
Defined in src/core/config/site.schema.ts:
type HeaderCtaConfig = {
label: string;
href: string;
variant?: "primary" | "secondary";
};
type HeaderSearchConfig = {
enabled: boolean;
behavior: "modal" | "page";
href?: string;
};
type HeaderConfig = {
navPosition: "left" | "center" | "right";
stickyHeader: "none" | "sticky" | "scroll-up";
mobileMenuPosition: "left" | "right" | "full-width";
search: HeaderSearchConfig;
cta?: HeaderCtaConfig;
};
Defaults: navPosition: 'right', stickyHeader: 'scroll-up', mobileMenuPosition: 'right', search: { enabled: true, behavior: 'modal' }, cta: undefined.
lib/theme.ts
src/lib/theme.ts exports setTheme(mode) and getTheme(). Imported directly by vanilla TypeScript controllers that manage theme switching.
setTheme(mode)— writeslocalStorage['theme-preference'], setsdata-themeon<html>, dispatchestheme-changedeventgetTheme()— reads from localStorage, returns'system'when absent