Skip to Content
PatternsFormsFilter Panel

Filter Panel

A collapsible filter panel with checkbox groups and active filter chips. Built using Box, Column, Row, Checkbox, Icon, Text, and Button components.

Filters
Category (1)
42
28
15
Price Range
23
31
19

Source

'use client'; import { ChevronDown, X } from 'lucide-react'; import { useState } from 'react'; import { Box, Button, Checkbox, Column, Icon, Row, Text } from '@umami/react-zen'; export function FilterPanel({ groups, selectedFilters, onFilterChange, onClearAll, }) { const totalSelected = Object.values(selectedFilters).flat().length; return ( <Column gap="4"> <Row alignItems="center" justifyContent="space-between"> <Text weight="semibold">Filters</Text> {totalSelected > 0 && ( <Button variant="quiet" size="sm" onPress={onClearAll}> Clear all ({totalSelected}) </Button> )} </Row> {groups.map((group) => ( <FilterGroupComponent key={group.id} group={group} selected={selectedFilters[group.id] || []} onFilterChange={(optionId, checked) => onFilterChange(group.id, optionId, checked)} /> ))} </Column> ); } function FilterGroupComponent({ group, selected, onFilterChange }) { const [isOpen, setIsOpen] = useState(true); return ( <Column> <Row alignItems="center" justifyContent="space-between" paddingY="2" className="cursor-pointer" onClick={() => setIsOpen(!isOpen)} > <Text weight="medium"> {group.label} {selected.length > 0 && <Text as="span" color="muted"> ({selected.length})</Text>} </Text> <Icon size="sm" color="muted" rotate={isOpen ? 180 : 0}> <ChevronDown /> </Icon> </Row> {isOpen && ( <Column gap="2" paddingLeft="1"> {group.options.map((option) => ( <Row key={option.id} alignItems="center" justifyContent="space-between"> <Checkbox isSelected={selected.includes(option.id)} onChange={(checked) => onFilterChange(option.id, checked)} > <Text>{option.label}</Text> </Checkbox> {option.count !== undefined && ( <Text color="muted">{option.count}</Text> )} </Row> ))} </Column> )} </Column> ); } export function ActiveFilters({ groups, selectedFilters, onRemove }) { const activeFilters = Object.entries(selectedFilters).flatMap(([groupId, optionIds]) => optionIds.map((optionId) => { const group = groups.find((g) => g.id === groupId); const option = group?.options.find((o) => o.id === optionId); return { groupId, optionId, label: option?.label || optionId }; }) ); if (activeFilters.length === 0) return null; return ( <Row gap="2" flexWrap="wrap"> {activeFilters.map(({ groupId, optionId, label }) => ( <Row key={`${groupId}-${optionId}`} alignItems="center" gap="1" paddingX="2" paddingY="1" backgroundColor="interactive" borderRadius="full" > <Text>{label}</Text> <Box className="cursor-pointer" onClick={() => onRemove(groupId, optionId)}> <Icon><X /></Icon> </Box> </Row> ))} </Row> ); }

Variations

With active filter chips

Electronics
Clothing
Apple

Collapsed by default

Filters
Category
Price Range