SearchBar

A compound search bar component for location, dates, and guests selection.

Installation

pnpm add @wandercom/design-system-web

Usage

The SearchBar is a compound component that provides a flexible search interface. It consists of a collapsed bar with location trigger, date/guest buttons, and an expandable dropdown panel for location search. 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.

26 lines
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:

12 lines
import { SearchBar } from '@wandercom/design-system-web/ui/search-bar';

export function Example() {
  return (
    <SearchBar
      labels={{
        searchLabel: 'Search',
        locationLabel: 'Where',
      }}
    />
  );
}

Controlled location

Use the location and onLocationChange props to manage location state externally, e.g. syncing with a map view.

14 lines
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).

Examples

A complete search bar with curated empty-state content (recent searches and suggested regions) plus async-mode server search. Pass onLocationQueryChange to delegate filtering to the consumer (e.g. a remote search API) — the SearchBar debounces 200ms internally before invoking the callback. Combine 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, the list dims with aria-busy + reduced opacity until new results arrive.

Loading example...

With popover

The SearchBarPopover provides a dropdown for location search with sections and items using the Popover component.

35 lines
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>
  );
}

Composition with asChild

Use the asChild prop to render custom elements while maintaining functionality.

12 lines
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>
  );
}

Date 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.

62 lines
import {
  SearchBarButton,
  SearchBarDatePopoverCalendar,
  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>
  );
}

Guests popover

The SearchBarGuestsPopoverContent provides a stepper interface for selecting guests and pets. Click the "Who" button in the example above to see the guests popover.

37 lines
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>
  );
}

Props

className?:

string
Additional CSS classes to apply to the root container.

locations?:

SearchBarLocation[]
Location suggestions used for search matching.

units?:

SearchBarLocation[]
Property suggestions shown in the location panel.

recentSearches?:

SearchBarLocation[]
Recent search items shown when the query is empty.

suggestedRegions?:

SearchBarLocation[]
Suggested regions shown when the query is empty.

initialValues?:

Partial<SearchBarValues>
Initial values for location, dates, guests, and pets.

minDate?:

Date
Minimum selectable date. Defaults to today.

labels?:

SearchBarLabels
Optional label overrides for i18n and copy updates.

onSearch?:

(values: SearchBarValues) => void
Callback when the user triggers search.

location?:

SearchBarLocation | null
Controlled location value. When provided, overrides internal location state. Use this to sync with external state (e.g., map movements).

onLocationChange?:

(location: SearchBarLocation | null) => void
Callback when the location changes internally (user selects a suggestion). Use alongside location for controlled mode.

onLocationSelect?:

(location: SearchBarLocation) => void
Callback when a location suggestion is selected.

onLocationQueryChange?:

(query: string) => void
Callback when the location search query changes. Use this for server-side search.

locationSuggestions?:

SearchBarLocation[]
Server-provided location suggestions based on the current query.

unitSuggestions?:

SearchBarLocation[]
Server-provided unit/property suggestions based on the current query.

isLoadingSuggestions?:

boolean
Whether location suggestions are currently loading.

filtersContent?:

ReactNode
Content for the mobile filters sub-drawer.

filtersValue?:

string
Current filters value text displayed on the mobile filters trigger.

onClearFilters?:

() => void
Callback when the clear filters button is clicked.

desktopFiltersContent?:

ReactNode
Content for the desktop filters panel.

asChild?:

boolean
When true, uses Radix Slot for the outermost wrapper, allowing composition inside a parent element (e.g., a <form>).

inheritContainer?:

boolean
When true, responsive visibility follows the nearest ancestor query container rather than establishing a new container on the SearchBar root.

onLayoutChange?:

(isDesktop: boolean) => void
Callback after the responsive layout changes. Providing this overrides the composed SearchBar default layout-change reset handler.

SearchBarRoot

className?:

string
Additional CSS classes to apply to the root container.

desktop?:

ReactNode
Content to render on desktop (the pill-shaped collapsed bar). Falls back to children if not provided.

mobile?:

ReactNode
Content to render on mobile (typically the drawer trigger and drawer content). Falls back to children if not provided.

asChild?:

boolean
When true, uses Radix Slot for the outermost wrapper, allowing composition inside a parent element (e.g., a <form>).

inheritContainer?:

boolean
When true, responsive visibility follows the nearest ancestor query container rather than establishing a new container on the SearchBar root.

onLayoutChange?:

(isDesktop: boolean) => void
Callback after the responsive layout changes between desktop and mobile.

children?:

ReactNode
Fallback content used for both desktop and mobile when those props are not provided.

SearchBarDesktop

className?:

string
Additional CSS classes to apply.

children:

ReactNode
SearchBar segments (LocationTrigger, Button, ActionArea). Dividers are automatically added between children.

activeSegment?:

'location' | 'dates' | 'guests' | null
The currently active segment for external state management.

onActiveSegmentChange?:

(segment: 'location' | 'dates' | 'guests' | null) => void
Callback when the active segment changes.

hasLocationValue?:

boolean
Whether the location field has a value. Affects filled state styling.

hasDatesValue?:

boolean
Whether the dates field has a value. Affects filled state styling.

hasGuestsValue?:

boolean
Whether the guests field has a value. Affects filled state styling.

SearchBarLocationTrigger

active?:

boolean
When true, shows elevated state with white background and shadow.

asChild?:

boolean
Renders child element and merges props using Radix Slot.

className?:

string
Additional CSS classes to apply.

label?:

string
Label shown when active and empty. Defaults to "Where".

placeholder?:

string
Label shown when inactive and empty. Defaults to "Where".

SearchBarButton

asChild?:

boolean
Renders child element and merges props using Radix Slot.

active?:

boolean
When true, shows the elevated/active state.

segment?:

'dates' | 'guests'
The segment this button represents. Enables context integration for active state and auto-labeling.

className?:

string
Additional CSS classes to apply.

label?:

string
Label shown when active and empty. Defaults to "When" for dates, "Who" for guests.

placeholder?:

string
Label shown when inactive and empty. Defaults to "When" for dates, "Who" for guests.

SearchBarActionArea

className?:

string
Additional CSS classes to apply.

children:

ReactNode
Usually contains a SearchBarButton and SearchBarSearchButton.

SearchBarSearchButton

className?:

string
Additional CSS classes to apply.

children?:

ReactNode
Button content. Defaults to "Search".

SearchBarPopover

Re-exported Radix Popover component for controlling open/closed state.

SearchBarPopoverTrigger

Re-exported PopoverTrigger for triggering the popover. Use with asChild prop.

SearchBarPopoverAnchor

Re-exported PopoverAnchor for custom anchor positioning.

SearchBarPopoverContent

Extends all PopoverContent props from Radix.

className?:

string
Additional CSS classes to apply.

children:

ReactNode
Popover content (PopoverInput, PopoverSection, etc.).

align?:

"start" | "center" | "end"
Alignment relative to the trigger. Defaults to "center".

sideOffset?:

number
Distance from the trigger in pixels. Defaults to 8.

SearchBarPopoverInput

placeholder?:

string
Input placeholder text. Defaults to "Search locations...".

className?:

string
Additional CSS classes to apply.

onClear?:

() => void
Callback when the clear button is clicked. When provided, a clear button appears when the input has a value.

onSubmit?:

() => void
Callback when Enter is pressed. Use this to advance to the next segment.

SearchBarPopoverSection

label?:

string
Section label displayed above items.

maxItems?:

number
Maximum number of items to display in this section. Defaults to 3.

className?:

string
Additional CSS classes to apply.

children:

ReactNode
Section content (SearchBarPopoverItem components).

SearchBarPopoverItem

title:

string
Primary text for the item.

subtitle?:

string
Secondary text displayed below the title.

icon?:

ReactNode
Custom icon to display. Shows map pin icon by default.

image?:

string
Image URL to display instead of an icon.

selected?:

boolean
Whether the item is currently selected.

asChild?:

boolean
Renders child element and merges props using Radix Slot.

onSelect?:

(location: { title: string; subtitle?: string }) => void
Callback when the item is selected. Receives the title and subtitle.

className?:

string
Additional CSS classes to apply.

SearchBarDatePopoverContent

Extends all PopoverContent props from Radix.

className?:

string
Additional CSS classes to apply.

children:

ReactNode
Popover content (Header, Calendar, Flexibility components).

align?:

"start" | "center" | "end"
Alignment relative to the trigger. Defaults to "center".

sideOffset?:

number
Distance from the trigger in pixels. Defaults to 8.

SearchBarDatePopoverHeader

mode?:

'dates' | 'flexible'
Current date mode. Defaults to 'dates'.

onModeChange?:

(mode: 'dates' | 'flexible') => void
Callback when the mode changes.

onClear?:

() => void
Callback when the clear button is clicked.

showClear?:

boolean
Whether to show the clear button. Defaults to true.

className?:

string
Additional CSS classes to apply.

datesLabel?:

string
Label for the Dates tab. Defaults to "Dates".

flexibleLabel?:

string
Label for the Flexible tab. Defaults to "Flexible".

clearLabel?:

string
Label for the clear action. Defaults to "Clear dates".

SearchBarDatePopoverCalendar

Extends most DayPickerProps from react-day-picker (excluding mode, numberOfMonths, and showOutsideDays).

selected?:

DateRange
The currently selected date range ({ from?: Date, to?: Date }).

onSelect?:

(range: DateRange | undefined) => void
Callback when a date range is selected.

endMonth?:

Date
The last month the calendar can navigate to. Defaults to 2 years from current month.

disabled?:

Matcher | Matcher[]
Days to disable (e.g., { before: new Date() }). Inherited from DayPickerProps.

className?:

string
Additional CSS classes to apply. Inherited from DayPickerProps.

SearchBarDatePopoverFlexibility

value?:

'exact' | '1' | '2' | '3' | '7'
The selected flexibility option. Defaults to 'exact'.

onValueChange?:

(value: 'exact' | '1' | '2' | '3' | '7') => void
Callback when the flexibility option changes.

disabled?:

boolean
Whether the flexibility options are disabled. Defaults to false.

className?:

string
Additional CSS classes to apply.

exactLabel?:

string
Label for the exact dates option. Defaults to "Exact dates".

oneDayLabel?:

string
Label for the ± 1 day option. Defaults to "± 1 day".

twoDaysLabel?:

string
Label for the ± 2 days option. Defaults to "± 2 days".

threeDaysLabel?:

string
Label for the ± 3 days option. Defaults to "± 3 days".

sevenDaysLabel?:

string
Label for the ± 7 days option. Defaults to "± 7 days".

SearchBarDatePopoverDuration

value?:

'weekend' | 'week'
The selected duration option. Defaults to 'weekend'.

onValueChange?:

(value: 'weekend' | 'week') => void
Callback when the duration option changes.

className?:

string
Additional CSS classes to apply.

titleLabel?:

string
Heading above the duration toggle. Defaults to "How long would you like to stay?".

weekendLabel?:

string
Label for the weekend option. Defaults to "Weekend".

weekLabel?:

string
Label for the week option. Defaults to "Week".

SearchBarDatePopoverMonthGrid

selected?:

SearchBarDateMonth[]
Array of selected months ({ month: number, year: number }).

onSelect?:

(months: SearchBarDateMonth[]) => void
Callback when month selection changes.

monthCount?:

number
Number of months to display in the grid. Defaults to 12.

startDate?:

Date
Starting date for the month grid. Defaults to current month.

titleLabel?:

string
Heading above the month grid. Defaults to "When do you want to go?".

className?:

string
Additional CSS classes to apply.

SearchBarGuestsPopoverContent

Extends all PopoverContent props from Radix.

className?:

string
Additional CSS classes to apply.

children:

ReactNode
Popover content (SearchBarGuestsPopoverRow components).

align?:

"start" | "center" | "end"
Alignment relative to the trigger. Defaults to "center".

sideOffset?:

number
Distance from the trigger in pixels. Defaults to 8.

SearchBarGuestsPopoverRow

label:

string
The label displayed on the left side of the row.

value:

number | null
The current count value. Null displays "Any" placeholder.

onChange:

(value: number | null) => void
Callback when the value changes.

min?:

number
Minimum allowed value. Defaults to 0.

max?:

number
Maximum allowed value. Defaults to 99.

className?:

string
Additional CSS classes to apply.

SearchBarMobileDrawer

className?:

string
Additional CSS classes for the trigger wrapper.

children?:

ReactNode
Content to render inside the drawer (SearchBarMobileCard components).

open?:

boolean
Whether the drawer is open.

onOpenChange?:

(open: boolean) => void
Callback when the drawer open state changes.

trigger?:

ReactNode
The trigger element that opens the drawer.

activeSegment?:

'location' | 'dates' | 'guests'
The currently active section. Defaults to 'location'.

onActiveSegmentChange?:

(segment: 'location' | 'dates' | 'guests') => void
Callback when the active section changes.

hasLocationValue?:

boolean
Whether the location field has a value.

hasDatesValue?:

boolean
Whether the dates field has a value.

hasGuestsValue?:

boolean
Whether the guests field has a value.

onSearch?:

() => void
Callback when the search button is clicked.

titleLabel?:

string
Screen-reader title for the drawer. Defaults to "Search".

filtersLabel?:

string
Label for the filters trigger. Defaults to "Filters?".

filtersPlaceholder?:

string
Placeholder text when no filters are selected. Defaults to "No filters".

filtersValue?:

string
Current filters value text.

filtersContent?:

ReactNode
Content for the filters sub-drawer.

onClearFilters?:

() => void
Callback when the clear filters button is clicked.

filtersTitleLabel?:

string
Title for the filters sub-drawer. Defaults to "Filters".

clearFiltersLabel?:

string
Label for the clear filters button. Defaults to "Clear filter".

clearSearchLabel?:

string
Accessible label for the clear button in the location search input. Defaults to "Clear search".

closeSearchLabel?:

string
Accessible label for the close button on the mobile search drawer. Defaults to "Close search".

closeFiltersLabel?:

string
Accessible label for the close button on the mobile filters drawer. Defaults to "Close filters".

showResultsLabel?:

string
Label for the show results button. Defaults to "Show results".

loadingSuggestionsLabel?:

string
Label shown while location suggestions are loading. Defaults to "Loading...".

unsetValuePlaceholder?:

string
Placeholder shown when a number input (guests, pets) has no value set. Defaults to "Any".

SearchBarMobileFooter

className?:

string
Additional CSS classes to apply.

onSkip?:

() => void
Callback when the skip button is clicked.

showSkip?:

boolean
Whether to show the skip button. Defaults to false.

searchLabel?:

string
Label for the search button. Defaults to "Search".

nextLabel?:

string
Label for the next button. Defaults to "Next".

skipLabel?:

string
Label for the skip button. Defaults to "Skip".

SearchBarMobileTrigger

className?:

string
Additional CSS classes to apply.

placeholder?:

string
Text displayed when empty. Defaults to "Start your search".

location?:

string
Location value to display (e.g., "San Francisco").

dates?:

string
Dates value to display. Defaults to "When".

guests?:

string
Guests value to display. Defaults to "Who".

SearchBarMobileCard

section?:

'location' | 'dates' | 'guests'
The section this card represents. Enables automatic accordion behavior within SearchBarMobileDrawer.

label:

string
Label displayed at the top when expanded or on the left when collapsed.

value?:

string
Value displayed on the right side when collapsed.

expanded?:

boolean
Whether the card is expanded. Auto-determined when using section prop.

onClick?:

() => void
Callback when the collapsed card is clicked.

headerAction?:

ReactNode
Action element in the header (e.g., "Clear dates" button). Only shown when expanded.

autoFocus?:

boolean
When true, focuses the first input inside the card after the entry animation completes. Defaults to true.

className?:

string
Additional CSS classes to apply.

children?:

ReactNode
Content rendered when the card is expanded.

SearchBarMobileCardContent

className?:

string
Additional CSS classes to apply.

children?:

ReactNode
Content to render inside the card.

SearchBarMobileCalendar

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?:

DateRange
The currently selected date range.

onSelect?:

(range: DateRange | undefined) => void
Callback when a date range is selected.

mode?:

'dates' | 'flexible'
Current date mode. Defaults to 'dates'.

onModeChange?:

(mode: 'dates' | 'flexible') => void
Callback when the mode changes.

flexibility?:

'exact' | '1' | '2' | '3' | '7'
The flexibility option value. Defaults to 'exact'.

onFlexibilityChange?:

(value: 'exact' | '1' | '2' | '3' | '7') => void
Callback when the flexibility option changes.

numberOfMonths?:

number
Number of months to display. Defaults to 12.

minDate?:

Date
The minimum selectable date.

duration?:

'weekend' | 'week'
The duration option for flexible dates. Defaults to 'weekend'.

onDurationChange?:

(value: 'weekend' | 'week') => void
Callback when the duration option changes (flexible mode).

selectedMonths?:

SearchBarDateMonth[]
The selected months for flexible dates.

onSelectedMonthsChange?:

(months: SearchBarDateMonth[]) => void
Callback when selected months change (flexible mode).

labels?:

object
Optional label overrides for i18n: datesTabLabel, flexibleTabLabel, durationTitleLabel, monthTitleLabel, weekendLabel, weekLabel, exactLabel, oneDayLabel, twoDaysLabel, threeDaysLabel, sevenDaysLabel.

className?:

string
Additional CSS classes to apply.

SearchBarFallback

A lightweight static fallback for use with Suspense boundaries or initial server renders. See the SearchBar Fallback documentation for full details.

Accessibility

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-selected for 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-label attributes (e.g., "Increase guests", "Decrease pets")
  • Counter values use aria-live="polite" to announce changes to screen readers
  • Disabled buttons are properly marked with disabled attribute
SearchBar