- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- ChatContainer
- ChatInput
- ChatMessage
- ChatMultiChoiceQuestion
- ChatMultiOptionQuestion
- ChatThinking
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- FilePicker
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Markdown
- Modal
- NativeSelect
- NumberInput
- OptionSlider
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Table
- Tabs
- Text
- Textarea
- TimePicker
- Toast
- Toggle
- ToggleCard
- ToggleGroup
- Toolbar
- 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. When its container is at least 65rem (1040px) wide, menu items are rendered as horizontal navigation with dropdowns for sections. Below that width, opening the menu expands the header height with a 480ms cubic-bezier(0.4, 0, 0.6, 1) reveal while the hamburger icon morphs into an X.
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' },
{ 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' },
{ 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.
At container widths of 65rem (1040px) and wider, menu items render inline as horizontal navigation. Sections become Dropdown menus with hover and click interaction; direct links render as ghost button links.
On mobile, opening the menu expands the header to fill the viewport with a 480ms cubic-bezier(0.4, 0, 0.6, 1) height transition; items are revealed by overflow-hidden clipping as the header grows, with no per-item stagger. The hamburger icon morphs into an X. Sections become expandable accordions, and links display as list items.
Accessibility and behavior: the menu locks body scroll while open, exposes ARIA labels on buttons, and respects prefers-reduced-motion.
Use mobileActionButton to render a different CTA in narrow header containers. This is useful when the desktop CTA is too wide or when mobile users need a different primary action. Keep sign-in actions inside the menu.
<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">Explore stays</Button>}
menuItems={menuItems}
/>When mobileActionButton is not provided, the header falls back to actionButton at all container widths. Pass null to omit the CTA in the narrow layout. The user avatar is hidden in the narrow layout regardless.
Use centerContent to place content such as a SearchBar in the center of the header row. The content is absolutely centered, so constrain its width for narrow and wide header containers. Pass inheritContainer to SearchBar so it follows the Header query container.
<Header
centerContent={
<div className="w-[calc(100cqw-7.5rem)] @min-[65rem]:w-[calc(100cqw-29rem)]">
<SearchBar inheritContainer />
</div>
}
logo={<Logo className="h-6 w-auto text-primary" />}
mobileActionButton={null}
/>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}
/>Pages with dark heroes above the fold need header content that reads against the dark image. Enable invertOnTransparent to force the header into dark mode while the background is transparent; once scrollEffect swaps to a solid background, the header follows the page theme again.
<Header
logo={<Logo className="h-6 w-auto text-primary" />}
actionButton={<Button variant="secondary" size="md">List on Wander</Button>}
scrollEffect
invertOnTransparent
menuItems={menuItems}
/>Only light-mode pages visibly invert — in dark mode the header content is already light-on-dark, so the prop is a no-op.
Pair with per-route logic on the consumer to apply invertOnTransparent only on pages that actually have a dark hero. Route groups or a thin AppHeader wrapper are both good fits.
Live example
Open in fullscreen, scroll past the hero, and toggle invertOnTransparent to see the failure case (header content vanishes against the dark image).
For headers that need to flip dark while overlapping dark sections anywhere on the page (not just transparent heroes at the top), use the useHeaderThemeSync hook directly in a custom header. The built-in Header block does not expose this as a prop — invertOnTransparent covers the common hero case.
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>
);
}An IntersectionObserver keyed to the header's height detects overlapping dark sections, and a MutationObserver picks up sections added or re-themed after mount. Returns "dark" when the header overlaps a matching element, "light" when it does not, or undefined before the first check or when disabled.
An options object is accepted 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. Returnsundefinedwhen disabled.selector(default[data-theme="dark"]:not([data-theme-scope="local"])) — CSS selector for elements that trigger the dark theme.
Scoping with data-theme-scope
For locally-themed components (e.g. a dark card or tooltip inside a light page) that should not trigger the sync, add data-theme-scope="local" alongside data-theme="dark":
{/* This WILL trigger the sync */}
<section data-theme="dark" className="bg-black text-white py-24">
<h2>Full-width dark hero</h2>
</section>
{/* This will NOT trigger the sync */}
<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>logo?:
logoIcon?:
actionButton?:
mobileActionButton?:
centerContent?:
user?:
avatarHref?:
menuItems?:
menuCtaButton?:
menuOpen?:
onMenuOpenChange?:
layout?:
scrollEffect?:
invertOnTransparent?:
className?:
containerClassName?:
avatarSrc?:
avatarAlt:
fullName?:
MenuItem is a discriminated union of MenuSection, MenuLink, MenuAction, and MenuSeparator. MenuSection, MenuLink, and MenuAction each support an optional hideOnMobile?: boolean field that omits the item from the mobile menu while keeping it visible on desktop.
MenuSection (expandable section with sub-items):
type:
label:
items:
hideOnMobile?:
MenuLink (direct navigation link):
type:
label:
href:
hideOnMobile?:
MenuAction (direct action):
type:
label:
onClick:
hideOnMobile?:
MenuSeparator (visual divider between groups):
type:
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)}
/>
);
}