SearchBar
A compound search bar component for location, dates, and guests selection.
pnpm add @wandercom/design-system-web
The SearchBar is a compound component that provides a flexible search interface. It consists of a collapsed desktop bar with location, date, guest, and search controls; opening a segment reveals a non-modal popover that morphs between the location, dates, and guests panels. By default it switches between mobile and desktop content from its own 65rem (1040px) query container. Pass inheritContainer when composing it inside an existing responsive container such as a Header.
import {
SearchBar,
SearchBarActionArea,
SearchBarButton,
SearchBarDesktop,
SearchBarLocationTrigger,
SearchBarRoot,
SearchBarSearchButton,
} from '@wandercom/design-system-web/ui/search-bar';
export function Example() {
return (
<SearchBarRoot
desktop={
<SearchBarDesktop>
<SearchBarLocationTrigger>Where</SearchBarLocationTrigger>
<SearchBarButton>Anytime</SearchBarButton>
<SearchBarActionArea>
<SearchBarButton>Any guests</SearchBarButton>
<SearchBarSearchButton />
</SearchBarActionArea>
</SearchBarDesktop>
}
/>
);
}Use SearchBar for the full experience with built-in panels and labels:
import { SearchBar } from '@wandercom/design-system-web/ui/search-bar';
export function Example() {
return (
<SearchBar
labels={{
searchLabel: 'Search',
locationLabel: 'Where',
}}
/>
);
}The mobile trigger is 48px tall in every context, including both mobile search experiences. SearchBarFallbackMobile uses the same height to avoid layout shift during loading.
Set mobileExperience="multi-screen" to opt into separate summary, Where, When, and Who screens. The default remains "single-screen"; desktop uses the same experience in either case. The existing container breakpoint chooses the mobile layout.
<SearchBar
mobileExperience="multi-screen"
labels={{ applyLabel: 'Apply', backLabel: 'Back', anyGuestsLabel: 'Any guest' }}
onMobileOpenChange={(open, experience) => {
// Consumers can record exposure for the assignment actually displayed.
}}
onSearch={handleSearch}
/>The experience is frozen until dismissal. Opening creates a draft of the current values. Apply commits one editor to the summary; Back discards that editor's unapplied changes. Selecting a destination immediately returns to the summary. Closing discards the whole draft, while Search submits it. Browser Back steps through editor, summary, and closed without modifying the URL. Updated initialValues hydrate dates and guest values while the full-screen flow is closed; use controlled location for destination changes.
The lifecycle callback reports both opening and closing and identifies the experience displayed. Assignment and exposure tracking remain consumer-owned; this package does not include experiment infrastructure.
Use the location and onLocationChange props to manage location state externally, e.g. syncing with a map view.
import { SearchBar, type SearchBarLocation } from '@wandercom/design-system-web/ui/search-bar';
import { useState } from 'react';
export function ControlledLocationExample() {
const [location, setLocation] = useState<SearchBarLocation | null>(null);
return (
<SearchBar
location={location}
onLocationChange={setLocation}
onSearch={(values) => console.log(values)}
/>
);
}When location is omitted, the component manages its own state internally (uncontrolled mode, initialized from initialValues).
initialValues is read when the SearchBar mounts. Use the controlled location prop when the preset needs to change later.
On desktop, the SearchBar popovers are non-modal. They move focus predictably without trapping keyboard users inside the popup.
Enter/SpaceonWhere,When, orWhoopens that segment and moves focus to the first interactive control in the popup.Escapecloses the open popup and returns focus to the segment that opened it.Shift + Tabfrom the first interactive control in a popup closes it and returns focus to the opener.Tabfrom the last interactive control in a popup closes it and moves focus to the next enabled SearchBar control (Where->When->Who->Search). If no later SearchBar control exists, focus continues to the next tabbable element after the SearchBar.- In the
Wherepopup,ArrowDownmoves from the search input into the first visible result and then through visible results.ArrowUpmoves to the previous result, returning to the search input from the first result.
A complete search bar with curated empty-state content (recent searches and suggested destinations) plus async location and linked property results. Pass onLocationQueryChange to delegate filtering to the consumer (e.g. a remote search API). The SearchBar debounces 200ms internally before invoking the callback. Combine it with locationSuggestions, unitSuggestions, and isLoadingSuggestions to render results and loading state.
The loading label renders immediately on keystroke (no flash of "No results found" during the debounce window). If previous suggestions are visible when the user types again, they keep their default styles, remain interactive, and expose aria-busy until new results arrive. Type Wander to see linked properties, which open in a new tab and use labels.propertyLinkLabelTemplate for their accessible name.
Use initialValues.location to set an uncontrolled location when the SearchBar first mounts.
The SearchBarPopover provides a dropdown for location search with sections and items using the Popover component.
import {
SearchBarPopover,
SearchBarPopoverContent,
SearchBarPopoverInput,
SearchBarPopoverItem,
SearchBarPopoverSection,
SearchBarPopoverTrigger,
SearchBarLocationTrigger,
} from '@wandercom/design-system-web/ui/search-bar';
import { useState } from 'react';
export function LocationPopover() {
const [open, setOpen] = useState(false);
return (
<SearchBarPopover open={open} onOpenChange={setOpen}>
<SearchBarPopoverTrigger asChild>
<SearchBarLocationTrigger active={open}>Where</SearchBarLocationTrigger>
</SearchBarPopoverTrigger>
<SearchBarPopoverContent>
<SearchBarPopoverInput placeholder="Search locations..." />
<SearchBarPopoverSection label="Recent searches">
<SearchBarPopoverItem
title="Los Angeles"
subtitle="Los Angeles, California"
/>
<SearchBarPopoverItem
title="Joshua Tree"
subtitle="California, United States"
/>
</SearchBarPopoverSection>
</SearchBarPopoverContent>
</SearchBarPopover>
);
}Use the asChild prop to render custom elements while maintaining functionality.
import { SearchBarLocationTrigger } from '@wandercom/design-system-web/ui/search-bar';
import { Popover, PopoverTrigger } from '@wandercom/design-system-web/ui/popover';
export function ComposedTrigger() {
return (
<Popover>
<SearchBarLocationTrigger asChild>
<PopoverTrigger>Where</PopoverTrigger>
</SearchBarLocationTrigger>
</Popover>
);
}The SearchBarDatePopoverContent provides a date range picker with calendar, mode toggle (Dates/Flexible), and flexibility options. Click the "Anytime" button in the example above to see the date popover.
import { SearchBarDatePopoverCalendar } from '@wandercom/design-system-web/ui/search-bar/date-calendar';
import {
SearchBarButton,
SearchBarDatePopoverContent,
SearchBarDatePopoverDuration,
SearchBarDatePopoverFlexibility,
SearchBarDatePopoverHeader,
SearchBarDatePopoverMonthGrid,
SearchBarPopover,
SearchBarPopoverTrigger,
type SearchBarDateDuration,
type SearchBarDateMonth,
} from '@wandercom/design-system-web/ui/search-bar';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';
export function DatePopover() {
const [open, setOpen] = useState(false);
const [mode, setMode] = useState<'dates' | 'flexible'>('dates');
const [dateRange, setDateRange] = useState<DateRange | undefined>();
const [flexibility, setFlexibility] = useState<'exact' | '1' | '2' | '3' | '7'>('exact');
const [duration, setDuration] = useState<SearchBarDateDuration>('weekend');
const [selectedMonths, setSelectedMonths] = useState<SearchBarDateMonth[]>([]);
return (
<SearchBarPopover open={open} onOpenChange={setOpen}>
<SearchBarPopoverTrigger asChild>
<SearchBarButton>Anytime</SearchBarButton>
</SearchBarPopoverTrigger>
<SearchBarDatePopoverContent>
<SearchBarDatePopoverHeader
mode={mode}
onModeChange={setMode}
onClear={() => {
setDateRange(undefined);
setSelectedMonths([]);
}}
/>
{mode === 'dates' && (
<>
<SearchBarDatePopoverCalendar
selected={dateRange}
onSelect={setDateRange}
disabled={{ before: new Date() }}
/>
<SearchBarDatePopoverFlexibility
value={flexibility}
onValueChange={setFlexibility}
disabled={!dateRange?.from || !dateRange?.to}
/>
</>
)}
{mode === 'flexible' && (
<div className="flex flex-col gap-6 px-7 py-7">
<SearchBarDatePopoverDuration value={duration} onValueChange={setDuration} />
<SearchBarDatePopoverMonthGrid selected={selectedMonths} onSelect={setSelectedMonths} />
</div>
)}
</SearchBarDatePopoverContent>
</SearchBarPopover>
);
}The SearchBarGuestsPopoverContent provides a stepper interface for selecting guests and pets. Click the "Who" button in the example above to see the guests popover.
import {
SearchBarButton,
SearchBarGuestsPopoverContent,
SearchBarGuestsPopoverRow,
SearchBarPopover,
SearchBarPopoverTrigger,
} from '@wandercom/design-system-web/ui/search-bar';
import { useState } from 'react';
export function GuestsPopover() {
const [open, setOpen] = useState(false);
const [guests, setGuests] = useState(1);
const [pets, setPets] = useState(0);
return (
<SearchBarPopover open={open} onOpenChange={setOpen}>
<SearchBarPopoverTrigger asChild>
<SearchBarButton>Any guests</SearchBarButton>
</SearchBarPopoverTrigger>
<SearchBarGuestsPopoverContent>
<SearchBarGuestsPopoverRow
label="Guests"
value={guests}
onChange={setGuests}
min={1}
max={16}
/>
<SearchBarGuestsPopoverRow
label="Pets"
value={pets}
onChange={setPets}
max={10}
/>
</SearchBarGuestsPopoverContent>
</SearchBarPopover>
);
}className?:
locations?:
recentSearches?:
suggestedRegions?:
initialValues?:
minDate?:
labels?:
onSearch?:
location?:
onLocationChange?:
onLocationSelect?:
onLocationQueryChange?:
locationSuggestions?:
isLoadingSuggestions?:
filtersContent?:
filtersValue?:
onClearFilters?:
desktopFiltersContent?:
asChild?:
inheritContainer?:
onLayoutChange?:
className?:
desktop?:
mobile?:
asChild?:
inheritContainer?:
onLayoutChange?:
children?:
className?:
children:
activeSegment?:
onActiveSegmentChange?:
hasLocationValue?:
hasDatesValue?:
hasGuestsValue?:
active?:
asChild?:
className?:
label?:
placeholder?:
asChild?:
active?:
segment?:
className?:
label?:
placeholder?:
className?:
children:
className?:
children?:
Re-exported Radix Popover component for controlling open/closed state.
Re-exported PopoverTrigger for triggering the popover. Use with asChild prop.
Re-exported PopoverAnchor for custom anchor positioning.
Extends all PopoverContent props from Radix.
className?:
children:
align?:
sideOffset?:
placeholder?:
className?:
onClear?:
onSubmit?:
label?:
maxItems?:
className?:
children:
title:
subtitle?:
icon?:
image?:
selected?:
asChild?:
onSelect?:
className?:
Extends all PopoverContent props from Radix.
className?:
children:
align?:
sideOffset?:
mode?:
onModeChange?:
onClear?:
showClear?:
className?:
datesLabel?:
flexibleLabel?:
clearLabel?:
Extends most DayPickerProps from react-day-picker (excluding mode, numberOfMonths, and showOutsideDays).
Import it from @wandercom/design-system-web/ui/search-bar/date-calendar. SearchBar loads it on demand, so it stays out of the startup bundle of apps that only render the composed search bar.
selected?:
onSelect?:
endMonth?:
disabled?:
className?:
value?:
onValueChange?:
disabled?:
className?:
exactLabel?:
oneDayLabel?:
twoDaysLabel?:
threeDaysLabel?:
sevenDaysLabel?:
value?:
onValueChange?:
className?:
titleLabel?:
weekendLabel?:
weekLabel?:
selected?:
onSelect?:
monthCount?:
startDate?:
titleLabel?:
className?:
Extends all PopoverContent props from Radix.
className?:
children:
align?:
sideOffset?:
label:
value:
onChange:
min?:
max?:
className?:
className?:
children?:
open?:
onOpenChange?:
trigger?:
activeSegment?:
onActiveSegmentChange?:
hasLocationValue?:
hasDatesValue?:
hasGuestsValue?:
onSearch?:
titleLabel?:
filtersLabel?:
filtersPlaceholder?:
filtersValue?:
filtersContent?:
onClearFilters?:
filtersTitleLabel?:
clearFiltersLabel?:
clearSearchLabel?:
closeSearchLabel?:
closeFiltersLabel?:
showResultsLabel?:
loadingSuggestionsLabel?:
unsetValuePlaceholder?:
className?:
onSkip?:
showSkip?:
searchLabel?:
nextLabel?:
skipLabel?:
className?:
placeholder?:
location?:
dates?:
guests?:
section?:
label:
value?:
expanded?:
onClick?:
headerAction?:
autoFocus?:
className?:
children?:
className?:
children?:
Import it from @wandercom/design-system-web/ui/search-bar/mobile-calendar. Like the desktop calendar, SearchBar loads it on demand.
A self-contained calendar experience for the mobile drawer, including a mode toggle (Dates/Flexible), date range calendar with day-of-week labels, flexibility selector, duration toggle, and month grid.
selected?:
onSelect?:
mode?:
onModeChange?:
flexibility?:
onFlexibilityChange?:
numberOfMonths?:
minDate?:
duration?:
onDurationChange?:
selectedMonths?:
onSelectedMonthsChange?:
labels?:
className?:
A lightweight static fallback for use with Suspense boundaries or initial server renders. See the SearchBar Fallback documentation for full details.
The SearchBar component includes proper accessibility features:
- Uses semantic button elements for interactive triggers
- Dividers are marked with
aria-hidden="true" - Panel items support
aria-selectedfor selection state - All interactive elements are keyboard accessible
- Focus states are visible with proper styling
- Screen reader compatible with proper text labels
- Stepper buttons have descriptive
aria-labelattributes (e.g., "Increase guests", "Decrease pets") - Counter values use
aria-live="polite"to announce changes to screen readers - Disabled buttons are properly marked with
disabledattribute