SearchFilters
Composable filter sections for search interfaces with price distribution, amenities, rooms, and property type sections. Available as SearchFiltersContent (headless) and SearchFiltersModal (with a fullscreen-mobile Modal presentation).
pnpm add @wandercom/design-system-web
import { SearchFiltersModal } from '@wandercom/design-system-web/blocks/search-filters';
import { Button } from '@wandercom/design-system-web/ui/button';
export function Example() {
const [priceRange, setPriceRange] = useState<[number, number]>([100, 800]);
const [amenities, setAmenities] = useState<string[]>([]);
const [bedrooms, setBedrooms] = useState<number | null>(null);
const [bathrooms, setBathrooms] = useState<number | null>(null);
const [propertyTypes, setPropertyTypes] = useState<string[]>([]);
return (
<SearchFiltersModal
trigger={<Button variant="outline">Filters</Button>}
price={{
data: priceDistribution,
value: priceRange,
onChange: setPriceRange,
min: 0,
max: 1000,
currency: "$",
}}
amenities={{
options: [
{ key: "wifi", label: "Wi-Fi" },
{ key: "pool", label: "Pool" },
{ key: "hot-tub", label: "Hot Tub" },
],
value: amenities,
onChange: setAmenities,
}}
rooms={{
bedrooms,
bathrooms,
onBedroomsChange: setBedrooms,
onBathroomsChange: setBathrooms,
}}
propertyTypes={{
options: [
{ key: "house", label: "House" },
{ key: "apartment", label: "Apartment" },
{ key: "cabin", label: "Cabin" },
],
value: propertyTypes,
onChange: setPropertyTypes,
}}
primaryAction={{ label: "Show results", action: () => undefined }}
secondaryAction={{ label: "Clear all", action: () => undefined }}
/>
);
}All filter sections are optional. Only sections with provided configuration are rendered, separated by dividers. State is fully managed by the consumer.
SearchFiltersModal uses Modal primitives at every viewport. It fills the mobile viewport with fade-only motion and switches to the standard centered Modal presentation at md and above.
The tiers section renders selectable cards (ToggleCardGroup) between price and amenities. Like every other section it is data-driven — pass options, value, and onChange.
<SearchFiltersModal
trigger={<Button variant="outline">Filters</Button>}
price={priceConfig}
tiers={{
options: [
{
value: "ICONIC",
label: "Iconic",
description: "Exceptional properties unlike any other",
icon: <IconCrown />,
},
{
value: "PREMIER",
label: "Premier",
description: "Top-rated stays with premium amenities",
icon: <IconDiamond />,
},
],
value: tiers,
onChange: setTiers,
}}
amenities={amenitiesConfig}
/>Each filter section accepts an optional label prop to override the default heading.
<SearchFiltersModal
trigger={<Button variant="outline">Filters</Button>}
price={{ ...priceConfig, label: "Nightly rate" }}
amenities={{ ...amenitiesConfig, label: "Must-haves" }}
rooms={{ ...roomsConfig, label: "Sleeping arrangements" }}
propertyTypes={{ ...propertyTypesConfig, label: "Stay type" }}
/>Use children to add custom filter sections below the built-in ones.
<SearchFiltersModal
trigger={<Button variant="outline">Filters</Button>}
price={priceConfig}
>
<div className="flex flex-col gap-2">
<span className="font-semibold text-body-sm">Rating</span>
{/* custom rating filter UI */}
</div>
</SearchFiltersModal>Use getSearchFiltersLabel to derive a summary string from filter state for the SearchBar's filtersValue prop. Returns undefined when no filters are active, falling through to the default placeholder.
import { SearchFiltersContent, SearchFiltersModal, getSearchFiltersLabel } from '@wandercom/design-system-web/blocks/search-filters';
<SearchBar
filtersValue={getSearchFiltersLabel({
amenities,
bedrooms,
bathrooms,
propertyTypes,
priceRange,
priceMin: 0,
priceMax: 1000,
})}
filtersContent={
<SearchFiltersContent
price={priceConfig}
amenities={amenitiesConfig}
rooms={roomsConfig}
propertyTypes={propertyTypesConfig}
/>
}
desktopFiltersContent={
<SearchFiltersModal
trigger={<Button variant="outline">Filters</Button>}
price={priceConfig}
amenities={amenitiesConfig}
rooms={roomsConfig}
propertyTypes={propertyTypesConfig}
primaryAction={{ label: "Show results", action: handleApply }}
secondaryAction={{ label: "Clear all", action: handleClear }}
/>
}
/>open?:
onOpenChange?:
trigger?:
title?:
price?:
tiers?:
amenities?:
rooms?:
propertyTypes?:
primaryAction?:
secondaryAction?:
className?:
children?:
data?:
value?:
onChange?:
min?:
max?:
step?:
currency?:
label?:
Rendered with ToggleCardGroup (checkbox-style multi-select cards). The section appears between price and amenities.