Header
betav0.1.0The application top bar: a structural container that owns the banner landmark, the 68px height, the shared background gradient at Figma's 94% opacity over a 4px backdrop blur, the bottom hairline and the start/middle/end distribution — and nothing else. Everything visible inside it (search, assistant button, notifications, account menu, workspace picker, status indicators) is product content passed as children, the same structure-and-slots contract that made Navbar work; the cross-product audit found the seven real top bars share a shape, not a payload. It carries NO positioning: the design doctrine is that the bar is always fixed to the top of the screen and never sticky, but the top offset varies per product (stacked banners), so the fixed offset and normal flow are both reachable through className and both are proven reproducible in a real browser; a positioned consumer passes fullWidth={false} rather than undoing a w-full, and offsets its content with the published --fds-header-height rather than restating 68px. HeaderGroup is the one sub-part — a horizontal cluster, growing or hugging. Read the banner usage rule in Header.rfc.md section 6.1 before adopting: a page must expose exactly one banner, and a section heading inside <main> must never be built with this component.
Variants
Workspace: Filigran
Examples
<Header><HeaderGroup grow><SearchField fullWidth /></HeaderGroup><HeaderGroup>{actions}</HeaderGroup></Header><Header fullWidth={false} className="fixed top-0 left-45 right-0 z-40">…</Header><HeaderGroup grow="unbounded" className="max-w-170">…</HeaderGroup><HeaderGroup><Button variant="ia">Ask Ariane</Button><HeaderGroup separatorBefore className="gap-0">{accountMenu}</HeaderGroup></HeaderGroup>Props
| Prop | Type |
|---|---|
as | Element to render. Defaults to 'header'. Overriding it REMOVES the banner landmark — only for the documented nested case (RFC 6.1). |
fullWidth | Whether the bar stretches to its containing block. Defaults to true. Pass false when the consumer positions the bar itself and supplies its own horizontal edges — a dynamic inline `left` is invisible to tailwind-merge, so a w-full would survive a className override and overflow (RFC 6.4, Trap 1). |
className | Merged through cn(). Carries the product's positioning, offset and z-index — the component ships none. |
children | Product content, normally HeaderGroup clusters. |
HeaderGroup.grow | false (default) hugs the content. true grows and caps at Figma's 400px. "unbounded" grows with no cap — min-w-0 flex-1 only — for a product whose own minimum exceeds 400px; the consumer then supplies the window, e.g. className="max-w-170". |
HeaderGroup.separatorBefore | Draw Figma's vertical rule on this cluster's leading edge (node 6968:20936): 1px, --border-elevation-subtle at 50%, the height of the action row, 16px clear each side. Defaults to false, so no existing bar gains a rule. It belongs to the cluster that FOLLOWS it, hence 'before' — wrap the account cluster, the frame the node draws it against. Decorative: a ::before, so no DOM node, never announced, never focusable. No side/colour/height/spacing option: the node binds one of each (RFC 3.7). |
HeaderGroup.className | Merged through cn(). gap-2 is the default cluster gap and any gap-* override wins through tailwind-merge, so there is no gap prop. |
HeaderSearch (opt-in) | The bar's integrated global search: the library's SearchField with the library's ButtonGroup seated inside it, as the growing middle cluster (node 2823:1330). OPT-IN — nothing renders it for you, so a Header that does not use it is unchanged; products turn it on at their next pin bump. It exists because Sandy overruled Header.rfc.md 4.2 on 2026-08-28: that section made global search a slot, but it was written before SearchField existed, so the slot was a workaround for a missing component rather than a rule about where composition belongs. It is a part you place, not a prop on Header, because Figma puts the search BETWEEN the two other clusters and Header distributes with justify-between — a prop would have to split children to land there. |
HeaderSearch.modes | The mode toggles rendered INSIDE the field, flush against its trailing edge (Figma buttonGroup, three items). Each needs value, a 16x16 icon and a label — the toggles are icon-only, so label is their accessible name (WCAG 4.1.2). Omit modes entirely for a plain header search: the field then keeps its normal trailing padding, since the flush edge exists only to seat the group. Pair with mode / onModeChange / modesLabel. They are a radio group: one tab stop for the run, arrow keys between segments. |
HeaderSearch.value | Search text (controlled), paired with onValueChange. onClear is REQUIRED to get a clear cross while controlled — SearchField will not overwrite a value it does not own. onSubmit fires on Enter. |
HeaderSearch.disabled | Disables the field AND every toggle from one prop. The field is outlined rather than filled in that state since the 2026-08-28 redesign. A single toggle can be disabled on its own through modes[].disabled without disabling the field. |
HeaderSearch.grow | Forwarded to HeaderGroup. Defaults to true — Figma's 400px cap. Pass "unbounded" for a bar whose own minimum exceeds it, e.g. OpenAEV. |
Accessibility
Renders a native <header> which, not being nested in article/aside/main/nav/section, exposes the banner landmark implicitly — no role attribute is set, because an explicit role=banner on a <header> is redundant and ARIA's first rule says not to. That landmark is the accessibility gain of adopting this component: of the twelve <header> render sites measured across the three products only one is a banner today, and MUI's AppBar contributed the landmark by accident rather than by contract. THE USAGE RULE IS NORMATIVE, not advisory (Header.rfc.md 6.1): exactly one banner per page, and section headings inside <main> must use a heading and a plain element, never this component. The `as` prop exists for the documented nested case and REMOVES the landmark — that is its entire effect. No aria-label is set or required: a single banner per page needs no distinguishing name, and a required label would push every product to invent one. SEPARATOR: HeaderGroup's opt-in leading rule is decorative and drawn as a ::before, so it adds no DOM node at all — nothing to announce, nothing for Tab to reach, no role=separator for a refactor to attach. It is never the sole carrier of meaning: the two clusters it divides are already met as separate groups of controls in DOM order. KEYBOARD: nothing of its own. Tab reaches the interactive children in DOM order, which is why HeaderGroup is a plain <div> and NOT role=toolbar — a toolbar imposes a roving tabindex, collapsing the whole bar to a single tab stop, the one keyboard behaviour a top bar must not have. This is also why @radix-ui/react-toolbar was rejected rather than adopted. READING ORDER: the DOM order is start, middle, end, matching the visual order in both LTR and RTL, so no tabindex is needed anywhere. The bar precedes <main> in the DOM in the fixed composition as well as in flow, so a screen-reader user meets it where they expect it. SC 1.4.10 REFLOW: the bar is a flex row with a growing middle group whose min-w-0 lets it shrink below its content's intrinsic width; the height is 68px by design — published as the --fds-header-height custom property so a host can retarget it and consumers can offset against it — and the products' own content decides what to drop at narrow widths, which the component deliberately does not arbitrate.