Header

Site header with logo, action button, responsive desktop navigation, and built-in animated mobile menu.

Installation

pnpm add @wandercom/design-system-web

Usage

The Header block provides a fixed navigation bar at the top of your site with a logo, action button, responsive desktop navigation, and built-in mobile menu. On desktop (md breakpoint and up), menu items are rendered as horizontal navigation with dropdowns for sections. On mobile, the menu features a staggered animation with a hamburger icon that morphs into an X when opened.

Render the header conditionally based on auth state: pass user and avatarHref when signed in, and use different menu item arrays for signed-in vs signed-out (e.g. "Sign in or sign up" vs "View profile", "Sign out").

"use client";

import { Button, Header, Logo } from '@wandercom/design-system-web';
import { Avatar } from '@wandercom/design-system-web/ui/avatar';
import type { MenuItem } from '@wandercom/design-system-web/blocks/header';
import { useTheme } from 'next-themes';

const getSignedOutMenuItems = (handleThemeChange: () => void): MenuItem[] => [
  { type: 'link', label: 'Sign in or sign up', href: '/auth/signin', hideOnMobile: true },
  { type: 'separator', hideOnMobile: true },
  { type: 'link', label: 'Download mobile app', href: '/app' },
  { type: 'link', label: 'List on Wander', href: '/list' },
  { type: 'link', label: 'Visit help center', href: '/help' },
  { type: 'separator' },
  { type: 'action', label: 'Change theme', onClick: handleThemeChange },
];

const getSignedInMenuItems = (handleThemeChange: () => void): MenuItem[] => [
  { type: 'link', label: 'View profile', href: '/profile', hideOnMobile: true },
  { type: 'link', label: 'View trips', href: '/trips' },
  { type: 'link', label: 'View wishlist', href: '/wishlist' },
  { type: 'link', label: 'Chat with concierge', href: '/concierge' },
  { type: 'link', label: 'Try AI search', href: '/search' },
  { type: 'separator' },
  { type: 'link', label: 'Download mobile app', href: '/app' },
  { type: 'link', label: 'List on Wander', href: '/list' },
  { type: 'link', label: 'Visit help center', href: '/help' },
  { type: 'separator' },
  { type: 'action', label: 'Change theme', onClick: handleThemeChange },
  { type: 'separator', hideOnMobile: true },
  { type: 'link', label: 'Sign out', href: '/auth/signout', hideOnMobile: true },
];

export function AppHeader() {
  const { resolvedTheme, setTheme } = useTheme();
  const signedIn = true; // from your auth provider
  const handleThemeChange = () => {
    setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
  };

  return (
    <Header
      actionButton={<Button size="md" variant="secondary">List on Wander</Button>}
      avatarHref={signedIn ? '/profile' : undefined}
      logo={<Logo className="h-6 w-auto text-primary" />}
      logoIcon={<Logo className="h-[22px] w-auto text-primary" variant="logomark" />}
      menuCtaButton={
        signedIn ? (
          <div className="flex flex-col gap-3">
            <Button asChild size="lg" variant="outline">
              <a href="/profile" className="flex items-center gap-2">
                <Avatar alt="User name" fullName="User name" size="sm" src="/avatar.jpg" />
                User name
              </a>
            </Button>
            <Button asChild size="lg" variant="primary">
              <a href="/auth/signout">Sign out</a>
            </Button>
          </div>
        ) : (
          <Button asChild size="lg" variant="primary">
            <a href="/auth/signin">Sign in or sign up</a>
          </Button>
        )
      }
      menuItems={signedIn ? getSignedInMenuItems(handleThemeChange) : getSignedOutMenuItems(handleThemeChange)}
      user={signedIn ? { avatarSrc: '/avatar.jpg', avatarAlt: 'User name', fullName: 'User name' } : undefined}
    />
  );
}

Example

The Header is best viewed in fullscreen. Open in a new tab and use the controls in the bottom-left to switch between signed-in and signed-out states and theme.

Loading example...

Desktop and Mobile Navigation

Menu items are rendered responsively:

Desktop (md+): Sections render as dropdown menus using the Dropdown component. Direct links render as ghost button links in horizontal navigation.

Mobile: All menu items render in a full-screen animated overlay. Sections are expandable accordions, and links are displayed as list items. The hamburger menu button is hidden on desktop (md+) and only shown on mobile.

Mobile Action Button

Use mobileActionButton to render a different CTA on mobile viewports. This is useful when the desktop CTA is too wide or when mobile users need a different call to action (e.g. a compact icon button or a sign-in prompt).

<Header
  logo={<Logo className="h-6 w-auto text-primary" />}
  actionButton={<Button variant="secondary" size="md">List on Wander</Button>}
  mobileActionButton={<Button variant="primary" size="md">Sign in</Button>}
  menuItems={menuItems}
/>

When mobileActionButton is not provided, the header falls back to actionButton on all viewports. The user avatar is hidden on mobile regardless.

Scroll Effect

Enable scrollEffect to add a background and bottom border when the user scrolls past the top of the page. The transition uses ease-out-cubic over 200ms.

<Header
  logo={<Logo className="h-6 w-auto text-primary" />}
  actionButton={<Button variant="secondary" size="md">List on Wander</Button>}
  scrollEffect
  menuItems={menuItems}
/>

Dark Section Theme Sync

Enable themeSync to automatically flip the header to dark mode when it overlaps a section with data-theme="dark".

<Header
  logo={<Logo className="h-6 w-auto text-primary" />}
  actionButton={<Button variant="secondary" size="md">List on Wander</Button>}
  themeSync
  menuItems={menuItems}
/>

{/* Somewhere below in the page */}
<section data-theme="dark" className="bg-black text-white py-24">
  <h2>Dark hero section</h2>
</section>

The hook uses getBoundingClientRect() intersection checks on passive scroll/resize events and a MutationObserver to detect dynamically added dark sections.

Scoping with data-theme-scope

By default, the header reacts to every data-theme="dark" element on the page. If you have a locally-themed component (e.g. a dark card or tooltip inside a light page) that should not trigger the header to flip, add data-theme-scope="local" alongside data-theme="dark":

{/* This WILL trigger the header to flip */}
<section data-theme="dark" className="bg-black text-white py-24">
  <h2>Full-width dark hero</h2>
</section>

{/* This will NOT trigger the header */}
<div data-theme="dark" data-theme-scope="local" className="rounded-lg bg-black p-6">
  <p>Dark card that doesn't affect the header</p>
</div>

The underlying selector is [data-theme="dark"]:not([data-theme-scope="local"]), so only page-level dark sections participate in the theme sync.

useHeaderThemeSync hook

The themeSync prop delegates to the useHeaderThemeSync hook from @wandercom/design-system-shared. You can use the hook directly for custom header implementations:

import { useHeaderThemeSync } from '@wandercom/design-system-shared/hooks/use-header-theme-sync';

function CustomHeader() {
  const headerRef = useRef<HTMLElement>(null);
  const theme = useHeaderThemeSync(headerRef);

  return (
    <header ref={headerRef} data-theme={theme}>
      {/* ... */}
    </header>
  );
}

The hook accepts an options object as the second argument:

const theme = useHeaderThemeSync(headerRef, {
  enabled: true,
  selector: '[data-theme="dark"]:not([data-theme-scope="local"])',
});
  • enabled (default true) — set to false to disable observation entirely. The hook returns undefined when disabled.
  • selector — CSS selector for elements that trigger the dark theme. Override this to match a different attribute pattern if needed.

The hook returns "dark" when the header overlaps a matching element, "light" when it does not, or undefined before the first check runs or when disabled.

Responsive Navigation Features

Desktop Navigation (md+)

  • Horizontal menu items rendered inline
  • Section items use Dropdown component with hover/click interaction
  • Direct links rendered as ghost button links
  • Hamburger menu hidden on desktop

Mobile Menu (below md)

  • Full-screen animated overlay
  • Smooth fade and slide-in transitions
  • Staggered item animations for visual polish
  • Hamburger icon morphs into X icon
  • Expandable sections with smooth height transitions
  • Respects prefers-reduced-motion setting

Accessibility

  • Focus trap within open menu
  • Escape key to close
  • Proper ARIA labels on buttons
  • Keyboard navigation support
  • Screen reader compatible

Behavior

  • Prevents body scroll when menu open
  • Click outside or Escape to close mobile menu
  • Sections expand/collapse on click (mobile)
  • Dropdown menus on desktop with proper positioning

Migration from onMenuClick

The onMenuClick prop is deprecated. Migrate to the built-in menu system using menuItems for better user experience and consistency.

Before (deprecated):

<Header
  logo={<Logo />}
  actionButton={<Button>Get Started</Button>}
  onMenuClick={() => setMenuOpen(true)}
/>

After (recommended):

<Header
  logo={<Logo />}
  actionButton={<Button>Get Started</Button>}
  menuItems={[
    { type: 'link', label: 'About', href: '/about' },
    { type: 'link', label: 'Contact', href: '/contact' },
  ]}
/>

Props

logo?:

React.ReactNode
Logo content displayed on the left side. Can be the Logo component, an image, SVG, or any React node.

actionButton?:

React.ReactNode
CTA button displayed on the right side of the header. Typically a Button component prompting user action. On mobile, falls back to this when mobileActionButton is not provided.

mobileActionButton?:

React.ReactNode
Mobile-specific CTA button. When provided, replaces actionButton on mobile viewports. Falls back to actionButton when absent.

user?:

HeaderUser
User information for signed-in state. When provided, displays an avatar (40px) between the action button and menu button.

avatarHref?:

string
URL for the avatar link. When provided, the avatar becomes clickable. Typically links to the user profile or account settings.

onMenuClick?:

() => void
DEPRECATED: Use menuItems prop instead. Callback fired when the menu button is clicked. Maintained for backward compatibility.

layout?:

"default" | "wide" | "full"
Layout style for the Container wrapping the header content. Defaults to "default".

scrollEffect?:

boolean
Enables background and border transition on scroll. When true, the header gains a solid background and bottom border after the user scrolls past the top of the page. Default false.

themeSync?:

boolean
Enables automatic dark-section theme syncing. When true, the header applies data-theme="dark" when it overlaps any element matching [data-theme="dark"]. Default false.

className?:

string
Additional CSS classes to apply to the header element.

containerClassName?:

string
Additional CSS classes to apply to the Container wrapping the header content.

HeaderUser

avatarSrc?:

string
User avatar image URL.

avatarAlt:

string
Alt text for the avatar image. Required for accessibility.

fullName?:

string
User full name for fallback display when no avatar image is provided.

MenuItem is a discriminated union of MenuSection, MenuLink, and MenuAction.

MenuSection (expandable section with sub-items):

type:

"section"
Identifies this item as an expandable section.

label:

string
Display text for the section header.

items:

Array<{ label: string; href: string }>
Array of link items within the section. Each item has a label and href.

MenuLink (direct navigation link):

type:

"link"
Identifies this item as a simple navigation link.

label:

string
Display text for the link.

href:

string
URL the link navigates to.

MenuAction (direct action):

type:

"action"
Identifies this item as a simple action.

label:

string
Display text for the action.

onClick:

() => void
Action to run when selected.

Mobile

pnpm add @wandercom/design-system-mobile

Mobile Usage

The mobile header uses the logomark (icon) variant of the logo for a more compact display. It includes a built-in menu button.

import { Header } from '@wandercom/design-system-mobile/blocks/header';
import { Logo } from '@wandercom/design-system-mobile/ui/logo';

export function Example() {
  return (
    <Header
      logo={<Logo variant="logomark" height={22} width={21} color="#000" />}
      onMenuPress={() => setMenuOpen(true)}
    />
  );
}

Mobile Props

logo?:

React.ReactNode
Logo content displayed on the left side. Should use the logomark variant for mobile.

onMenuPress?:

() => void
Callback fired when the menu button is pressed. Use this to open your menu or navigation drawer.

className?:

string
Additional NativeWind classes to apply to the header.
Header