Components · App chrome
Header
The product top bar — brand or page title on the left, actions and the theme switcher on the right. A 56px sticky bar on the header surface with a hairline underline. This very page sits below the real one; the preview below is a static instance.
Preview
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Header" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `header` spec (variants: header, header-title, theme-button) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `header` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
┌─ header (56px) ────────────────────────────────────────────────────────────┐
│ [left slot] [center slot] [right slot] │
│ │
│ Website: Nav links Locale + CTA + Theme │
│ Logo (mark + wordmark) (up to 5 items) │
│ │
│ In-app: — Search + Notif + Locale │
│ Page title + Theme + User avatar │
│ │
│ Mobile (< 768px): — Theme switcher │
│ Hamburger + logo mark │
└─────────────────────────────────────────────────────────────────────────────┘
Parts:
header— sticky container, full viewport width, 56px height,z-index: 200header-logo— mark + wordmark lockup; mark-only in collapsed sidebar stateheader-title— in-app context only; current page name, single line, truncatesheader-nav— horizontal nav links; website context only; collapses to drawer on mobile or when items exceed 5header-actions— right slot flex row; hosts icon buttons and controlsheader-locale-button— globe + locale code + chevron; opens locale dropdownheader-theme-button— sun/moon icon; toggles light/dark/accessibility themeheader-user-button— 32px avatar; opens user menu dropdownheader-user-dropdown— info header + action items
| State | Visual |
|---|---|
| Default | Full header with nav, search, actions |
| Scrolled (sticky) | Shadow appears below header as page scrolls |
| Mobile | Collapsed nav — hamburger menu trigger visible |
| Mobile menu open | Navigation items expand as drawer or dropdown |
When to use
- In every authenticated page view as the top-level navigation and identity surface.
- In website/marketing pages as the primary brand bar and nav anchor.
When not to use
- Do not render two headers in the same view.
- Do not use the header as a section title inside a page — use
{typography.heading-1}or{typography.heading-2}instead. - Do not put primary content actions in the header — those belong in the page-level action slot or sidebar.
Do
- Keep the right slot to a maximum of 5 controls (search, notifications, locale, theme, user).
- Show the logo mark only (no wordmark) on viewports below 768px.
- Collapse website nav links to a drawer when items exceed 5 or viewport is below 768px.
- Use
header-nav-link-activestate for the currently active route — apply via router. - Keep the page title (in-app) to a single line and truncate with ellipsis before it reaches the right slot.
Don't
- Don't increase header height beyond 56px.
- Don't put more than one CTA button in the website navbar right slot.
- Don't render the full logo lockup (mark + wordmark) in the collapsed sidebar state or on mobile — use mark only.
- Don't place destructive actions in the header.
- Don't suppress the focus ring on any header interactive element.
- Page title (in-app): Match the route or section name exactly. Sentence case. No verbs. Maximum 40 characters before truncation kicks in.
- Nav links (website): Short nouns. Title case. 1–2 words maximum. ("Product", "Pricing", "Docs", "Blog").
- CTA label (website): Action verb + object. ("Get started", "Sign in", "Try free"). Maximum 3 words.
- Locale button label: 2-letter ISO 639-1 uppercase code only ("EN", "FR", "ID"). No flag emojis.
- Locale dropdown items: Full language name in that language ("English", "Français", "Bahasa Indonesia").
- User menu display name: Full name as stored in profile. Email shown below in muted style.
Keyboard
| Key | Behavior |
|---|---|
Tab |
Move focus through header interactive elements (logo, nav links, search, actions) |
Enter / Space |
Activate the focused link or button |
Arrow Down |
When a locale or user dropdown is open, move focus to first item |
Escape |
Close any open header dropdown or mobile menu, returning focus to the trigger |
Accessibility
- ARIA role:
<header>element withrole="banner". Nav links wrapped in<nav aria-label="Main navigation">. - Logo:
aria-label="Zetta — home"on the logo link. If mark-only, the wordmark text is visually hidden but present for screen readers. - Theme button:
aria-label="Switch to dark mode"/"Switch to light mode"— updates dynamically. - Locale button:
aria-label="Select language". Active locale shown witharia-checked="true"on the dropdown item. - User button:
aria-label="User menu"on the avatar trigger.aria-expandedreflects dropdown state. - Focus management: On dropdown open, focus moves to first menu item. On close (keyboard dismiss or click-outside), focus returns to trigger.
- Skip link: A visually hidden "Skip to main content" link must appear as the first focusable element before the header — product-level responsibility.
- Contrast: Logo mark (Lime
#b6d600) on white canvas is 1.67:1 — permitted only under the WCAG logotype exemption, not as a UI boundary. Wordmark ({colors.ink}) meets 19.8:1.
Spec
Values and token references resolved straight from the Zetta spec.
header
- backgroundColor
- {colors.surface-header}
- borderBottomColor
- {colors.hairline}
- borderBottomWidth
- 1px
- height
- 56px
- paddingX
- {spacing.page}
- display
- flex
- alignItems
- center
- justifyContent
- space-between
- position
- sticky
- top
- 0
- zIndex
- {elevation.z-index.navigation}
header-logo
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}
- markSize
- 24px
- markColor
- {colors.brand}
- wordmarkColor
- {colors.ink}
- fontFamily
- Inter
- fontSize
- 16px
- fontWeight
- 700
- lineHeight
- 1.3
- letterSpacing
- -0.01em
- textDecoration
- none
header-logo-dark
- markColor
- {colors.brand}
- wordmarkColor
- {logo.on-dark-color}
header-title
- textColor
- {colors.ink}
- fontFamily
- Inter
- fontSize
- 18px
- fontWeight
- 600
- lineHeight
- 1.35
- letterSpacing
- -0.01em
header-nav
- display
- flex
- alignItems
- center
- gap
- {spacing.lg}
header-nav-link
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- lineHeight
- 1.4
- textDecoration
- none
header-nav-link-hover
- textColor
- {colors.ink}
header-nav-link-active
- textColor
- {colors.ink}
- fontWeight
- 600
header-nav-link-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
header-actions
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}
header-theme-button
- iconSize
- {icons.sizeMap.md}
- iconColor
- {colors.muted}
header-theme-button-hover
- iconColor
- {colors.primary}
- backgroundColor
- {colors.primary-bg}
header-theme-button-active
- backgroundColor
- {colors.surface-pressed}
header-theme-button-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
header-locale-button
- display
- flex
- alignItems
- center
- gap
- {spacing.xs}
- height
- 32px
- paddingX
- {spacing.sm}
- borderRadius
- {rounded.base}
- backgroundColor
- transparent
- borderColor
- transparent
- borderWidth
- 1px
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- iconSize
- {icons.sizeMap.md}
- chevronSize
- {icons.sizeMap.sm}
- cursor
- pointer
header-locale-button-hover
- backgroundColor
- {colors.primary-bg}
- textColor
- {colors.primary}
header-locale-button-active
- backgroundColor
- {colors.surface-pressed}
header-locale-button-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
header-locale-dropdown
- minWidth
- 160px
header-user-button
- width
- 32px
- height
- 32px
- borderRadius
- {rounded.full}
- cursor
- pointer
header-user-button-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
header-user-dropdown
- minWidth
- 200px
- paddingY
- {spacing.xs}
header-user-dropdown-header
- paddingX
- {spacing.md}
- paddingY
- {spacing.sm}
- borderBottomColor
- {colors.hairline}
- borderBottomWidth
- 1px
- nameColor
- {colors.ink}
- nameFont
- Geist
- nameFontSize
- 14px
- nameFontWeight
- 500
- emailColor
- {colors.muted}
- emailFont
- Geist
- emailFontSize
- 12px
- emailFontWeight
- 400
header-cta
- value
- —
header-search-trigger
- height
- 32px
- paddingX
- {spacing.md}
- borderRadius
- {rounded.base}
- backgroundColor
- {colors.surface-secondary}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 13px
- minWidth
- 200px
- maxWidth
- 400px
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}
header-search-trigger-hover
- borderColor
- {colors.border-strong}
- backgroundColor
- {colors.surface-card}
header-notification-trigger
- position
- relative
- iconSize
- {icons.sizeMap.md}
- iconColor
- {colors.muted}
- borderRadius
- {rounded.base}
- padding
- {spacing.xs}
- cursor
- pointer
header-notification-badge
- position
- absolute
- top
- 2px
- right
- 2px
- minWidth
- 16px
- height
- 16px
- borderRadius
- {rounded.full}
- backgroundColor
- {colors.danger}
- textColor
- {colors.danger-text}
- fontFamily
- Geist
- fontSize
- 10px
- fontWeight
- 700
- display
- flex
- alignItems
- center
- justifyContent
- center
- paddingX
- 3px
- borderColor
- {colors.canvas}
- borderWidth
- 2px
header-workspace-trigger
- display
- flex
- alignItems
- center
- gap
- {spacing.xs}
- height
- 32px
- paddingX
- {spacing.sm}
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- textColor
- {colors.ink}
- cursor
- pointer
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- backgroundColor
- transparent
header-workspace-trigger-hover
- backgroundColor
- {colors.surface-secondary}
header-mega-menu
- position
- fixed
- top
- 56px
- left
- 0
- right
- 0
- backgroundColor
- {colors.surface-card}
- borderBottomColor
- {colors.hairline}
- borderBottomWidth
- 1px
- shadow
- {elevation.shadow.overlay}
- zIndex
- {elevation.z-index.popup}
- padding
- {spacing.page}
- display
- grid
- gridTemplateColumns
- repeat(4, 1fr)
- gap
- {spacing.lg}
header-mega-menu-section-title
- fontFamily
- Geist
- fontSize
- 11px
- fontWeight
- 600
- textColor
- {colors.muted}
- textTransform
- uppercase
- letterSpacing
- 0.06em
- paddingBottom
- {spacing.xs}
- borderBottomColor
- {colors.hairline}
- borderBottomWidth
- 1px
- marginBottom
- {spacing.sm}
Known gaps
- Sticky header scroll behavior — transparency / blur effect not yet specified.
- Notification panel layout and content spec — product-defined; no additional token gaps.