3.8 KiB
SiteHeader
Function
The chrome at the top of a marketing or documentation site: brand at one end,
navigation in the middle, actions at the other, and a drawer for the narrow
viewport. It pins to the top of its scroll host and knows it is pinned, so
the app can change its elevation or background on [data-stuck] without the
block owning any treatment.
Composition map
| Part | Composes | Key props |
|---|---|---|
| root | Sticky (F1.1) |
offset — passed through; skipped entirely when sticky={false} |
| measure | Container |
size (container prop, default lg) |
| the bar | Group |
justify="space-between" · align="center" |
brand / nav / actions |
— (app snippets) | the app composes Link, NavigationMenu, Button… |
mobileNav + mobileTrigger |
Drawer |
direction="end", bind:open ↔ mobileOpen |
Landmark + headings: emits a single <header> and NO heading — a site
header is a landmark, not a section, and the page's h1 belongs to whatever
follows (usually a hero). If the app puts several navigations on the page,
name them: the block passes aria-label straight through to the <header>,
and the nav snippet's own NavigationMenu takes its own name.
Decisions
2026-07-23 — reference floor (dossier §P1: Tailwind Plus «Headers 8 + Navbars 11 + Flyout 7» · Untitled UI · Flowbite):
- Adopted: the three-cluster bar (brand / nav / actions), the pinned state
as a visual axis, the mobile drawer, and the flyout navigation — the
dossier's explicit parity gap («sin flyout/dropdown de navegación no hay
paridad»). We do not re-implement it: the app passes a
NavigationMenu, which already ships the flyout, into thenavsnippet. - Discarded: the references' static HTML dumps of every arrangement. The bar is ONE layout; a different arrangement is the app's markup inside the snippets, not a variant prop.
- The announcement banner is NOT baked in: the dossier's sibling category
is served by the
bannerblock (F2.11) or by the canonBannerinchildren, which renders under the bar. One header, one function. stickyis a prop, not a variant: a header that does not pin still is this block; skipping theStickywrapper keeps the DOM honest (no wrapper that does nothing).
Gaps
| Gap | Disposition |
|---|---|
| Scroll-hide (header retreats when scrolling down) | canon candidate — it is behaviour with contract surface (direction detection), so it goes to sticky first, never into this block (admission rule) |
| Skip-link | shells own it — it belongs to the page shell (app-shell / docs-shell, F3/F4), which knows what the main region is; putting it here would emit a second one on every shell page |
| Search field in the bar | app-land — it is a Field/Command the app composes into actions; the block does not need to know |
| Sub-navigation row under the bar | out — children renders under the bar; a second row of links is markup, not new API |
| A NAVIGATING call-to-action that looks like a button | canon candidate — surfaced while composing this block: Button deliberately has no href («Link owns navigation», its README) and Link has no prominent variant, so the header's «Get started» can only be a text link today. The honest fix is one decision in the canon (a Link variant or Button href), never a block-local fake |
Found while composing
NavigationMenuemitted no landmark (2026-07-23): its morfo declaresdefaultElement: 'nav'for the Provider, but the soma component rendered a<div>— so the component that is supposed to BE the site's navigation had no<nav>at all, the exact a11y gap the dossier attributes to every reference. Fixed in the canon (soma/components/navigation-menu), not papered over here.