Site-header
Top-of-page chrome with four authored patterns selected via
data-pattern. Built on CSS grid + container queries:
- Architecture:
data-patternattribute swaps grid template-areas per pattern. All four patterns ship in v1.0. - Sticky: opt-in via
data-sticky="true". CSS-only, position:sticky. - Mobile collapse: container-query driven. Below the mobile
breakpoint, every pattern reduces to
logo | spacer | mobile-trigger; the consumer opens a Sheet from the trigger.
Patterns
| data-pattern | Layout | Use for |
|---|---|---|
standard (default) |
logo | nav (center) | actions | mobile | Marketing sites (Linear-style) |
inline |
logo | · | nav | actions | mobile | Product sites with right-aligned nav (Stripe-style) |
breadcrumb |
logo | breadcrumb-in-__nav | actions | mobile | App chrome (Notion-style) |
centered |
nav | logo (center) | actions | mobile | Retail / brand sites (Apple-style) |
Anatomy
<header class="site-header" data-pattern="standard" data-sticky="true">
<a class="site-header__logo" href="/">Malevich</a>
<nav class="site-header__nav" aria-label="Primary">
<a href="/docs">Docs</a>
<a href="/components">Components</a>
<a href="/blog">Blog</a>
</nav>
<div class="site-header__actions">
<button class="button -secondary -s">Sign in</button>
<button class="button -primary -s">Get started</button>
</div>
<button class="site-header__mobile-trigger" type="button"
aria-haspopup="dialog" aria-controls="mobile-menu"
aria-expanded="false" aria-label="Open menu">
☰
</button>
</header>
<dialog class="sheet" data-side="left" id="mobile-menu">
<header class="sheet__header">
<h2>Menu</h2>
<button class="sheet__close" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</header>
<div class="sheet__body">
<nav aria-label="Primary mobile">
<a href="/docs">Docs</a>
<!-- … -->
</nav>
</div>
</dialog>
The mobile-trigger button uses the same aria-haspopup="dialog" +
aria-controls contract as Popover, so the Popover runtime
auto-registers and wires up open/close + aria-expanded sync.
Sticky behavior
<header class="site-header" data-sticky="true">…</header>
CSS handles position: sticky; inset-block-start: 0. The header
sits above scrolling content at z-index 100. Backdrop blur applies
for the glassy look.
data-sticky is part of the behavior modifier category per
ADR-0007; this is the second runtime-less behavior (copyable was the
first behavior-with-runtime). For sticky, position:sticky natively
covers the use case — no JS required.
Tokens used
--color-surface-canvas— background--color-border-muted— bottom border--color-ink-strong,--color-ink-regular--color-surface-raised— nav link hover--space-inset-block-m,--space-inset-section-m--space-gap-elements-{s,m}--radius-button--border-width-hairline--font-display-title-*,--font-heading-section-*,--font-body-support-*--motion-fast,--motion-easing-default--size-control-m
Component-tier (generated)
--site-header-mobile-breakpoint(640px) — container-query threshold--site-header-sticky-blur(8px) — backdrop blur radius
Accessibility
- Use
<header>as the wrapper for the page-level header. <nav aria-label="Primary">identifies the navigation landmark.- Mobile trigger has
aria-haspopup="dialog"+aria-controlsand the linked Sheet is a native<dialog>(so focus management, Escape close, and inert background come for free). - Use
aria-current="page"on the active nav link.
Edge cases
- Container queries: require Chrome 105+, Safari 16+, Firefox 110+. In older browsers, the mobile collapse never triggers and the inline nav stays visible — degrades gracefully.
- Sticky + transforms: if an ancestor has
transform, position sticky stops working (browser limitation). Place the Site-header outside transformed ancestors.
Do
- Use real
<header>and<nav>semantics. - Pair the mobile trigger with a Sheet for the mobile menu.
- Set
aria-current="page"on the active nav link.
Don't
- Don't omit
aria-labelfrom the nav. - Don't write breakpoint-specific CSS in user stylesheets — the container query handles the collapse.