Scroll this section — the sticky header above stays pinned.
Long content fills space to demonstrate the sticky behavior...
/*
* Site-header — section
*
* Top-of-page chrome. Four patterns via data-pattern attribute:
*
* standard — Linear-style: logo left, nav center, actions right (default)
* inline — Stripe-style: logo left, nav + actions right
* breadcrumb — Notion-style: logo left, breadcrumb (in __nav slot) grows,
* actions right
* centered — Apple-style: logo centered, nav left of logo, actions right
*
* Sticky behavior is opt-in via data-sticky="true". Mobile collapse is
* container-query driven: below the mobile breakpoint, nav + actions
* hide and the mobile-trigger button appears.
*
* <header class="site-header" data-pattern="standard" data-sticky="true">
* <a class="site-header__logo" href="/">Logo</a>
* <nav class="site-header__nav" aria-label="Primary">
* <a href="/docs">Docs</a>
* </nav>
* <div class="site-header__actions">
* <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>
*/
@layer malevich.components {
.site-header {
container-type: inline-size;
container-name: site-header;
display: grid;
align-items: center;
gap: var(--space-gap-elements-m);
padding-block: var(--space-inset-block-m);
padding-inline: var(--space-inset-section-m);
background-color: var(--color-surface-canvas);
border-block-end: var(--border-width-hairline) solid var(--color-border-muted);
inline-size: 100%;
box-sizing: border-box;
/* Default layout = standard pattern */
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "logo nav actions mobile";
}
/* Sticky behavior modifier */
.site-header[data-sticky="true"] {
position: sticky;
inset-block-start: 0;
z-index: 100;
backdrop-filter: blur(var(--site-header-sticky-blur));
}
/* ---- Grid areas (common to all patterns) ---- */
.site-header__logo {
grid-area: logo;
display: inline-flex;
align-items: center;
color: var(--color-ink-strong);
text-decoration: none;
font-family: var(--font-display-title-family);
font-size: var(--font-heading-section-size);
font-weight: var(--font-display-title-weight);
line-height: var(--font-heading-section-line-height);
letter-spacing: var(--font-display-title-letter-spacing);
}
.site-header__nav {
grid-area: nav;
display: inline-flex;
align-items: center;
gap: var(--space-gap-elements-m);
min-inline-size: 0;
}
.site-header__nav a {
color: var(--color-ink-regular);
text-decoration: none;
padding-block: var(--space-inset-element-s);
padding-inline: var(--space-inset-element-s);
border-radius: var(--radius-button);
white-space: nowrap;
transition: color var(--motion-fast) var(--motion-easing-default),
background-color var(--motion-fast) var(--motion-easing-default);
font-family: var(--font-body-support-family);
font-size: var(--font-body-support-size);
font-weight: 500;
line-height: var(--font-body-support-line-height);
letter-spacing: var(--font-body-support-letter-spacing);
}
.site-header__nav a:hover {
color: var(--color-ink-strong);
background-color: var(--color-surface-raised);
}
.site-header__nav a[aria-current="page"] {
color: var(--color-ink-strong);
font-weight: 600;
}
.site-header__actions {
grid-area: actions;
display: inline-flex;
align-items: center;
gap: var(--space-gap-elements-s);
}
.site-header__mobile-trigger {
grid-area: mobile;
display: none;
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
padding: var(--space-inset-element-s);
margin: 0;
cursor: pointer;
color: var(--color-ink-strong);
inline-size: var(--size-control-m);
block-size: var(--size-control-m);
align-items: center;
justify-content: center;
border-radius: var(--radius-button);
}
.site-header__mobile-trigger:hover {
background-color: var(--color-surface-raised);
}
/* ---- Pattern: standard (default, Linear-style) ----
* logo | nav (center) | actions | mobile
*/
.site-header[data-pattern="standard"],
.site-header:not([data-pattern]) {
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "logo nav actions mobile";
}
.site-header[data-pattern="standard"] .site-header__nav,
.site-header:not([data-pattern]) .site-header__nav {
justify-self: center;
}
/* ---- Pattern: inline (Stripe-style) ----
* logo | spacer | nav | actions | mobile
*/
.site-header[data-pattern="inline"] {
grid-template-columns: auto 1fr auto auto auto;
grid-template-areas: "logo . nav actions mobile";
}
.site-header[data-pattern="inline"] .site-header__nav {
justify-self: end;
}
/* ---- Pattern: breadcrumb (Notion-style) ----
* logo | breadcrumb (in __nav, grows from start) | actions | mobile
* Authors place a <nav class="breadcrumb"> inside .site-header__nav.
*/
.site-header[data-pattern="breadcrumb"] {
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "logo nav actions mobile";
}
.site-header[data-pattern="breadcrumb"] .site-header__nav {
justify-self: start;
inline-size: 100%;
overflow: hidden;
}
/* ---- Pattern: centered (Apple-style) ----
* nav (left of logo) | logo (center) | actions (right) | mobile
*/
.site-header[data-pattern="centered"] {
grid-template-columns: 1fr auto 1fr auto;
grid-template-areas: "nav logo actions mobile";
}
.site-header[data-pattern="centered"] .site-header__logo {
justify-self: center;
}
.site-header[data-pattern="centered"] .site-header__nav {
justify-self: start;
}
.site-header[data-pattern="centered"] .site-header__actions {
justify-self: end;
}
/* ---- Container-aware mobile collapse ----
* Below the mobile breakpoint every pattern reduces to:
* logo | spacer | mobile-trigger
* Nav and actions are hidden; the consumer opens a Sheet from the
* trigger.
*/
@container site-header (max-width: 640px) {
.site-header,
.site-header[data-pattern] {
grid-template-columns: auto 1fr auto;
grid-template-areas: "logo . mobile";
}
.site-header__nav,
.site-header__actions {
display: none;
}
.site-header__mobile-trigger {
display: inline-flex;
}
}
}
This component is pure CSS — no JavaScript required.
# Site-Header — AGENTS.md
> Auto-generated from `site-header.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `site-header`
- **Layer:** sections
- **Status:** stable
- **Last updated:** 2026-05-19
## Summary
(no summary)
## Variants
| 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) |
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | `solid`, `gradient` |
| `surface` | `flat`, `elevated` |
| `effect` | `blur` |
| `shader` | — |
| `rhythm` | `compact`, `regular` |
| `motion` | any value |
| `behavior` | `sticky` |
## Anatomy
```html
<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.
## Tokens consumed
- `--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 contract
- 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-controls` and
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.
## Guidance
### 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-label` from the nav.
- Don't write breakpoint-specific CSS in user stylesheets — the
container query handles the collapse.