- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Modal
- NativeSelect
- NumberInput
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Tabs
- Text
- Textarea
- Toast
- Toggle
- ToggleGroup
- Tooltip
Header
Site header with logo, action button, responsive desktop navigation, and built-in animated mobile menu.
pnpm add @wandercom/design-system-web
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}
/>
);
}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.
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.
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.
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}
/>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(defaulttrue) — set tofalseto disable observation entirely. The hook returnsundefinedwhen 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.
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-motionsetting
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
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' },
]}
/>logo?:
actionButton?:
mobileActionButton?:
user?:
avatarHref?:
menuItems?:
menuCtaButton?:
onMenuClick?:
layout?:
scrollEffect?:
themeSync?:
className?:
containerClassName?:
avatarSrc?:
avatarAlt:
fullName?:
MenuItem is a discriminated union of MenuSection, MenuLink, and MenuAction.
MenuSection (expandable section with sub-items):
type:
label:
items:
MenuLink (direct navigation link):
type:
label:
href:
MenuAction (direct action):
type:
label:
onClick:
pnpm add @wandercom/design-system-mobile
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)}
/>
);
}