Filter
Filter is the building kit for list filtering: a trigger button, a popup where the user builds a selection, and removable chips for filters already applied. It holds no filter state — what is selected, what “apply” and “clear” mean, and how results are fetched all belong to the consuming page.
Code
import { Checkbox, Filter, FilterApply, FilterButton, FilterChip, FilterChipRemove, FilterChips, FilterClearAll, FilterContent, FilterFooter, FilterHeader, FilterNav, FilterNavFooter, FilterNavItem, FilterOption, FilterOptionList, FilterPane, FilterPaneBack, FilterPaneClear, FilterPaneFooter, FilterPaneHeader, FilterPaneTitle,} from "@falcon/ui-kit";import { useState } from "react";
const regions = ["Midwest", "Great Plains", "Delta", "Southeast", "Pacific"];
export function Example() { const [open, setOpen] = useState(false); const [draft, setDraft] = useState<string[]>([]); const [applied, setApplied] = useState<string[]>([]);
function toggle(region: string, checked: boolean) { setDraft(checked ? [...draft, region] : draft.filter((r) => r !== region)); }
function apply() { setApplied(draft); setOpen(false); }
function clearDraft() { setDraft([]); }
function clearApplied() { setApplied([]); setDraft([]); }
const draftCount = draft.length > 0 ? 1 : 0;
return ( <div className="tw:flex tw:flex-col tw:items-start tw:gap-2"> <Filter open={open} onOpenChange={(nextOpen) => { if (nextOpen) { setDraft(applied); } setOpen(nextOpen); }} > <FilterButton>Filter</FilterButton>
<FilterContent> <FilterHeader />
<FilterNav> <FilterNavItem active applied={draft.length > 0} values={draft}> Region </FilterNavItem> </FilterNav>
<FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Region</FilterPaneTitle>
{draft.length > 0 && <FilterPaneClear onClick={clearDraft} />} </FilterPaneHeader>
<FilterOptionList> {regions.map((region) => ( <FilterOption key={region}> <Checkbox checked={draft.includes(region)} onCheckedChange={(checked) => toggle(region, checked)} /> {region} </FilterOption> ))} </FilterOptionList>
<FilterFooter> <FilterClearAll onClick={clearDraft} /> <FilterApply count={draftCount} onClick={apply} /> </FilterFooter>
<FilterPaneFooter onClear={clearDraft} /> </FilterPane>
<FilterNavFooter> <FilterClearAll onClick={clearDraft}>Clear filters</FilterClearAll> <FilterApply count={draftCount} onClick={apply} /> </FilterNavFooter> </FilterContent> </Filter>
{applied.length > 0 && ( <FilterChips> <FilterChip category="Region" values={applied}> <FilterChipRemove aria-label="Remove region filter" onClick={clearApplied} /> </FilterChip> </FilterChips> )} </div> );}Composition
Section titled “Composition”Use the following composition to build a Filter:
Filter├── FilterButton└── FilterContent ├── FilterHeader ├── FilterNav │ ├── FilterNavSaved │ └── FilterNavItem ├── FilterPane │ ├── FilterPaneHeader │ │ ├── FilterPaneBack │ │ ├── FilterPaneTitle │ │ └── FilterPaneClear │ ├── FilterSearch │ ├── FilterOptionList │ │ └── FilterOption │ ├── FilterPaneContent │ ├── FilterFooter │ │ ├── FilterClearAll │ │ └── FilterApply │ └── FilterPaneFooter └── FilterNavFooter ├── FilterClearAll └── FilterApplyA pane body is either a FilterOptionList of FilterOption rows — with a
Checkbox for multi-select or RadioGroup items for
one-of-many — or a FilterPaneContent of free-form controls
(Select, Input). A “Select all” row is a FilterOption
with a Checkbox in its indeterminate state. Build pane bodies from
Falcon components only; when migrating an existing filter, rebuild each pane
from these blocks instead of pasting its old markup — foreign markup drags its
own typography, spacing, and colors into the popup.
Applied filters render outside the popup, and FilterSave opens a
FilterSaveDialog:
FilterChips├── FilterChip│ └── FilterChipRemove└── FilterSaveThe Saved pane lists FilterSavedItem rows inside a FilterOptionList:
FilterSavedItem├── FilterSavedItemButton├── FilterSavedItemEdit└── FilterSavedItemDeleteBasic usage
Section titled “Basic usage”A single category with a draft selection: choices accumulate in local state and
take effect on Apply, and the applied category renders as one FilterChip whose
remove clears it. A single-category filter renders its FilterPane
unconditionally, with no category state to manage.
Below the sm breakpoint the popup becomes two screens: the category nav and
the open pane. Wire the same apply-and-close onClick into both footers you
render yourself — the pane’s FilterFooter fires it on desktop,
FilterNavFooter on the mobile nav screen.
Code
import { Checkbox, Filter, FilterApply, FilterButton, FilterChip, FilterChipRemove, FilterChips, FilterClearAll, FilterContent, FilterFooter, FilterHeader, FilterNav, FilterNavFooter, FilterNavItem, FilterOption, FilterOptionList, FilterPane, FilterPaneBack, FilterPaneClear, FilterPaneFooter, FilterPaneHeader, FilterPaneTitle,} from "@falcon/ui-kit";import { useState } from "react";
const regions = ["Midwest", "Great Plains", "Delta", "Southeast", "Pacific"];
export function Example() { const [open, setOpen] = useState(false); const [draft, setDraft] = useState<string[]>([]); const [applied, setApplied] = useState<string[]>([]);
function toggle(region: string, checked: boolean) { setDraft(checked ? [...draft, region] : draft.filter((r) => r !== region)); }
function apply() { setApplied(draft); setOpen(false); }
function clearDraft() { setDraft([]); }
function clearApplied() { setApplied([]); setDraft([]); }
const draftCount = draft.length > 0 ? 1 : 0;
return ( <div className="tw:flex tw:flex-col tw:items-start tw:gap-2"> <Filter open={open} onOpenChange={(nextOpen) => { if (nextOpen) { setDraft(applied); } setOpen(nextOpen); }} > <FilterButton>Filter</FilterButton>
<FilterContent> <FilterHeader />
<FilterNav> <FilterNavItem active applied={draft.length > 0} values={draft}> Region </FilterNavItem> </FilterNav>
<FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Region</FilterPaneTitle>
{draft.length > 0 && <FilterPaneClear onClick={clearDraft} />} </FilterPaneHeader>
<FilterOptionList> {regions.map((region) => ( <FilterOption key={region}> <Checkbox checked={draft.includes(region)} onCheckedChange={(checked) => toggle(region, checked)} /> {region} </FilterOption> ))} </FilterOptionList>
<FilterFooter> <FilterClearAll onClick={clearDraft} /> <FilterApply count={draftCount} onClick={apply} /> </FilterFooter>
<FilterPaneFooter onClear={clearDraft} /> </FilterPane>
<FilterNavFooter> <FilterClearAll onClick={clearDraft}>Clear filters</FilterClearAll> <FilterApply count={draftCount} onClick={apply} /> </FilterNavFooter> </FilterContent> </Filter>
{applied.length > 0 && ( <FilterChips> <FilterChip category="Region" values={applied}> <FilterChipRemove aria-label="Remove region filter" onClick={clearApplied} /> </FilterChip> </FilterChips> )} </div> );}Saved filters
Section titled “Saved filters”Saved filters are optional. When a filter offers them, FilterNav leads with
FilterNavSaved, even when there is only one other category. The saved list
itself lives in the consumer — Falcon renders it but does not persist it.
With more than one category, FilterPaneBack returns to the nav without
clearing which category was open, so keep that state as is: FilterNavItem
still shows it active, and resizing back up to desktop shows that pane instead
of an empty second column.
Code
import { Checkbox, Filter, FilterApply, FilterButton, FilterChip, FilterChipRemove, FilterChips, FilterClearAll, FilterContent, FilterFooter, FilterHeader, FilterNav, FilterNavFooter, FilterNavItem, FilterNavSaved, FilterOption, FilterOptionList, FilterPane, FilterPaneBack, FilterPaneClear, FilterPaneFooter, FilterPaneHeader, FilterPaneTitle, FilterSave, FilterSaveDialog, FilterSavedItem, FilterSavedItemButton, FilterSavedItemDelete, FilterSavedItemEdit,} from "@falcon/ui-kit";import { useRef, useState } from "react";
const regions = ["Midwest", "Great Plains", "Delta", "Southeast", "Pacific"];
interface SavedFilter { id: number; name: string; regions: string[];}
export function Example() { const [open, setOpen] = useState(false); const [category, setCategory] = useState("Region"); const [draft, setDraft] = useState<string[]>([]); const [applied, setApplied] = useState<string[]>([]); const [saved, setSaved] = useState<SavedFilter[]>([]); const [saveOpen, setSaveOpen] = useState(false); const [editing, setEditing] = useState<SavedFilter | null>(null); const nextId = useRef(1);
function toggle(region: string, checked: boolean) { setDraft(checked ? [...draft, region] : draft.filter((r) => r !== region)); }
function apply() { setApplied(draft); setOpen(false); }
function clearDraft() { setDraft([]); }
function clearApplied() { setApplied([]); setDraft([]); }
const draftCount = draft.length > 0 ? 1 : 0;
const footer = ( <FilterFooter> <FilterClearAll onClick={clearDraft} /> <FilterApply count={draftCount} onClick={apply} /> </FilterFooter> );
const mobileFooter = <FilterPaneFooter onClear={clearDraft} />;
return ( <div className="tw:flex tw:flex-col tw:items-start tw:gap-2"> <Filter open={open} onOpenChange={(nextOpen) => { if (nextOpen) { setDraft(applied); } setOpen(nextOpen); }} > <FilterButton>Filter</FilterButton>
<FilterContent> <FilterHeader />
<FilterNav> <FilterNavSaved active={category === "Saved"} applied={saved.length > 0} onClick={() => setCategory("Saved")} />
<FilterNavItem active={category === "Region"} applied={draft.length > 0} values={draft} onClick={() => setCategory("Region")} > Region </FilterNavItem> </FilterNav>
{category === "Saved" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Saved</FilterPaneTitle> </FilterPaneHeader>
{saved.length === 0 ? ( <p className="tw:flex-1 tw:px-4 tw:py-2 tw:text-[0.8rem] tw:text-muted-foreground"> No saved filters yet. Use Save next to your active filter chips to add one. </p> ) : ( <FilterOptionList> {saved.map((filter) => ( <FilterSavedItem key={filter.id}> <FilterSavedItemButton onClick={() => { setApplied(filter.regions); setDraft(filter.regions); setOpen(false); }} > {filter.name} </FilterSavedItemButton>
<FilterSavedItemEdit aria-label={`Edit ${filter.name}`} onClick={() => setEditing(filter)} />
<FilterSavedItemDelete aria-label={`Delete ${filter.name}`} onClick={() => setSaved(saved.filter((f) => f.id !== filter.id)) } /> </FilterSavedItem> ))} </FilterOptionList> )}
{footer} {mobileFooter} </FilterPane> )}
{category === "Region" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Region</FilterPaneTitle>
{draft.length > 0 && <FilterPaneClear onClick={clearDraft} />} </FilterPaneHeader>
<FilterOptionList> {regions.map((region) => ( <FilterOption key={region}> <Checkbox checked={draft.includes(region)} onCheckedChange={(checked) => toggle(region, checked)} /> {region} </FilterOption> ))} </FilterOptionList>
{footer} {mobileFooter} </FilterPane> )}
<FilterNavFooter> <FilterClearAll onClick={clearDraft}>Clear filters</FilterClearAll> <FilterApply count={draftCount} onClick={apply} /> </FilterNavFooter> </FilterContent> </Filter>
{applied.length > 0 && ( <> <FilterChips> <FilterChip category="Region" values={applied}> <FilterChipRemove aria-label="Remove region filter" onClick={clearApplied} /> </FilterChip>
<FilterSave onClick={() => setSaveOpen(true)} /> </FilterChips>
<FilterSaveDialog open={saveOpen} onOpenChange={setSaveOpen} onSave={(name) => { setSaved([ ...saved, { id: nextId.current++, name, regions: applied }, ]); }} > <FilterChips> <FilterChip category="Region" values={applied} /> </FilterChips> </FilterSaveDialog> </> )}
<FilterSaveDialog open={editing !== null} onOpenChange={(isOpen) => { if (!isOpen) setEditing(null); }} onSave={(name) => { setSaved( saved.map((f) => (f.id === editing?.id ? { ...f, name } : f)), ); }} title="Edit Filter" submitLabel="Edit Filter" defaultName={editing?.name} > <FilterChips> <FilterChip category="Region" values={editing?.regions ?? []} /> </FilterChips> </FilterSaveDialog> </div> );}Complete example
Section titled “Complete example”All three pane kinds behind one FilterNav, with a draft selection that commits
on Apply. Each category with a selection counts as one filter: the Apply button
shows the live count, the category’s FilterNavItem gets applied, and applied
categories render as chips that clear one category each.
Code
import { Button, ButtonGroup, Checkbox, CircleQuestionMarkIcon, Filter, FilterApply, FilterButton, FilterChip, FilterChipRemove, FilterChips, FilterClearAll, FilterContent, FilterFooter, FilterHeader, FilterNav, FilterNavFooter, FilterNavItem, FilterNavSaved, FilterOption, FilterOptionList, FilterPane, FilterPaneBack, FilterPaneClear, FilterPaneContent, FilterPaneFooter, FilterPaneHeader, FilterPaneTitle, FilterSave, FilterSaveDialog, FilterSavedItem, FilterSavedItemButton, FilterSavedItemDelete, FilterSavedItemEdit, FilterSearch, Label, PlusIcon, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Trash2Icon,} from "@falcon/ui-kit";import { Fragment, useRef, useState } from "react";
const statuses = ["Any status", "Active", "Inactive"];const regions = [ "Midwest", "Great Plains", "Delta", "Southeast", "Northeast", "Pacific", "Mountain", "Southwest", "Corn Belt", "Wheat Belt", "Cotton Belt", "Lake States",];const actions = ["Bought", "Didn't buy", "Booked", "Didn't book"];
interface Condition { id: number; action: string; operator: string;}
interface Selection { status: string; regions: string[]; conditions: Condition[];}
interface SavedFilter { id: number; name: string; selection: Selection;}
function newCondition(id: number): Condition { return { id, action: "", operator: "and" };}
function emptySelection(conditionId: number): Selection { return { status: "Any status", regions: [], conditions: [newCondition(conditionId)], };}
function countFilters(selection: Selection) { return ( (selection.status === "Any status" ? 0 : 1) + (selection.regions.length > 0 ? 1 : 0) + (selection.conditions.some((c) => c.action) ? 1 : 0) );}
function SelectionChips({ selection, onRemove, children,}: { selection: Selection; onRemove?: (category: keyof Selection) => void; children?: React.ReactNode;}) { const conditions = selection.conditions.filter((c) => c.action);
return ( <FilterChips> {selection.status !== "Any status" && ( <FilterChip category="Status" values={[selection.status]}> {onRemove && ( <FilterChipRemove aria-label="Remove status filter" onClick={() => onRemove("status")} /> )} </FilterChip> )}
{selection.regions.length > 0 && ( <FilterChip category="Region" values={selection.regions}> {onRemove && ( <FilterChipRemove aria-label="Remove region filter" onClick={() => onRemove("regions")} /> )} </FilterChip> )}
{conditions.length > 0 && ( <FilterChip category="Sales and bookings" values={conditions.map((c) => c.action)} > {onRemove && ( <FilterChipRemove aria-label="Remove sales and bookings filter" onClick={() => onRemove("conditions")} /> )} </FilterChip> )}
{children} </FilterChips> );}
export function Example() { const [open, setOpen] = useState(false); const [category, setCategory] = useState("Status"); const [search, setSearch] = useState(""); const nextId = useRef(1); const [draft, setDraft] = useState(emptySelection(0)); const [applied, setApplied] = useState(draft); const [saved, setSaved] = useState<SavedFilter[]>([]); const [saveOpen, setSaveOpen] = useState(false); const [editing, setEditing] = useState<SavedFilter | null>(null);
const draftCount = countFilters(draft);
const visibleRegions = regions.filter((region) => region.toLowerCase().includes(search.trim().toLowerCase()), );
function updateCondition(id: number, patch: Partial<Condition>) { setDraft({ ...draft, conditions: draft.conditions.map((c) => c.id === id ? { ...c, ...patch } : c, ), }); }
function clearDraft() { setDraft(emptySelection(nextId.current++)); }
function clearStatus() { setDraft({ ...draft, status: "Any status" }); }
function clearRegions() { setDraft({ ...draft, regions: [] }); }
function clearConditions() { setDraft({ ...draft, conditions: [newCondition(nextId.current++)] }); }
function removeCategory(removed: keyof Selection) { const next = { ...applied }; if (removed === "status") next.status = "Any status"; if (removed === "regions") next.regions = []; if (removed === "conditions") next.conditions = [newCondition(nextId.current++)]; setApplied(next); setDraft(next); }
function apply() { setApplied(draft); setOpen(false); }
const footer = ( <FilterFooter> <FilterClearAll onClick={clearDraft} /> <FilterApply count={draftCount} onClick={apply} /> </FilterFooter> );
return ( <div className="tw:flex tw:flex-col tw:items-start tw:gap-2"> <Filter open={open} onOpenChange={(nextOpen) => { if (nextOpen) { setDraft(applied); } setOpen(nextOpen); }} > <FilterButton>Filter</FilterButton>
<FilterContent> <FilterHeader />
<FilterNav> <FilterNavSaved active={category === "Saved"} applied={saved.length > 0} onClick={() => setCategory("Saved")} />
<FilterNavItem active={category === "Status"} applied={draft.status !== "Any status"} values={draft.status !== "Any status" ? [draft.status] : []} onClick={() => setCategory("Status")} > Status </FilterNavItem>
<FilterNavItem active={category === "Region"} applied={draft.regions.length > 0} values={draft.regions} onClick={() => setCategory("Region")} > Region </FilterNavItem>
<FilterNavItem active={category === "Sales and bookings"} applied={draft.conditions.some((c) => c.action)} values={draft.conditions .filter((c) => c.action) .map((c) => c.action)} onClick={() => setCategory("Sales and bookings")} > Sales and bookings </FilterNavItem> </FilterNav>
{category === "Saved" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Saved</FilterPaneTitle> </FilterPaneHeader>
{saved.length === 0 ? ( <p className="tw:flex-1 tw:px-4 tw:py-2 tw:text-[0.8rem] tw:text-muted-foreground"> No saved filters yet. Use Save next to your active filter chips to add one. </p> ) : ( <FilterOptionList> {saved.map((filter) => ( <FilterSavedItem key={filter.id}> <FilterSavedItemButton onClick={() => { setApplied(filter.selection); setDraft(filter.selection); setOpen(false); }} > {filter.name} </FilterSavedItemButton>
<FilterSavedItemEdit aria-label={`Edit ${filter.name}`} onClick={() => setEditing(filter)} />
<FilterSavedItemDelete aria-label={`Delete ${filter.name}`} onClick={() => setSaved(saved.filter((f) => f.id !== filter.id)) } /> </FilterSavedItem> ))} </FilterOptionList> )}
{footer} <FilterPaneFooter onClear={clearDraft} /> </FilterPane> )}
{category === "Status" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Status</FilterPaneTitle>
{draft.status !== "Any status" && ( <FilterPaneClear onClick={clearStatus} /> )} </FilterPaneHeader>
<FilterOptionList> <RadioGroup value={draft.status} onValueChange={(status) => setDraft({ ...draft, status })} className="tw:gap-0.5" > {statuses.map((status) => ( <FilterOption key={status}> <RadioGroupItem value={status} /> {status} </FilterOption> ))} </RadioGroup> </FilterOptionList>
{footer} <FilterPaneFooter onClear={clearStatus} /> </FilterPane> )}
{category === "Region" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Region</FilterPaneTitle>
{draft.regions.length > 0 && ( <FilterPaneClear onClick={clearRegions} /> )} </FilterPaneHeader>
<FilterSearch value={search} onChange={(event) => setSearch(event.target.value)} />
<FilterOptionList> {visibleRegions.map((region) => ( <FilterOption key={region}> <Checkbox checked={draft.regions.includes(region)} onCheckedChange={(checked) => setDraft({ ...draft, regions: checked ? [...draft.regions, region] : draft.regions.filter((r) => r !== region), }) } /> {region} </FilterOption> ))} </FilterOptionList>
{footer} <FilterPaneFooter onClear={clearRegions} /> </FilterPane> )}
{category === "Sales and bookings" && ( <FilterPane> <FilterPaneHeader> <FilterPaneBack /> <FilterPaneTitle>Sales and bookings</FilterPaneTitle>
{(draft.conditions.some((c) => c.action) || draft.conditions.length > 1) && ( <FilterPaneClear onClick={clearConditions} /> )} </FilterPaneHeader>
<FilterPaneContent> {draft.conditions.map((condition) => ( <Fragment key={condition.id}> {condition.id !== draft.conditions[0].id && ( <> <Separator />
<div className="tw:flex tw:items-center tw:gap-1.5"> <ButtonGroup className="tw:flex-1 tw:*:flex-1"> <Button size="sm" variant="outline" aria-pressed={condition.operator === "and"} className="tw:aria-pressed:bg-muted" onClick={() => updateCondition(condition.id, { operator: "and", }) } > AND </Button>
<Button size="sm" variant="outline" aria-pressed={condition.operator === "or"} className="tw:aria-pressed:bg-muted" onClick={() => updateCondition(condition.id, { operator: "or", }) } > OR </Button> </ButtonGroup>
<Button variant="ghost" size="icon-sm" aria-label="Remove condition" className="tw:text-muted-foreground" onClick={() => setDraft({ ...draft, conditions: draft.conditions.filter( (c) => c.id !== condition.id, ), }) } > <Trash2Icon /> </Button> </div> </> )}
<div className="tw:flex tw:flex-col tw:gap-1.5"> <Label>Accounts that</Label>
<Select value={condition.action} onValueChange={(action) => updateCondition(condition.id, { action: action ?? "", }) } > <SelectTrigger className="tw:w-full"> <SelectValue placeholder="Select action" /> </SelectTrigger>
<SelectContent> {actions.map((action) => ( <SelectItem key={action} value={action}> {action} </SelectItem> ))} </SelectContent> </Select> </div> </Fragment> ))}
<Separator />
<Button variant="link" size="sm" className="tw:self-start tw:text-muted-foreground" onClick={() => setDraft({ ...draft, conditions: [ ...draft.conditions, newCondition(nextId.current++), ], }) } > <PlusIcon /> Add "and / or" condition <CircleQuestionMarkIcon /> </Button> </FilterPaneContent>
{footer} <FilterPaneFooter onClear={clearConditions} /> </FilterPane> )}
<FilterNavFooter> <FilterClearAll onClick={clearDraft}>Clear filters</FilterClearAll> <FilterApply count={draftCount} onClick={apply} /> </FilterNavFooter> </FilterContent> </Filter>
{countFilters(applied) > 0 && ( <> <SelectionChips selection={applied} onRemove={removeCategory}> <FilterSave onClick={() => setSaveOpen(true)} /> </SelectionChips>
<FilterSaveDialog open={saveOpen} onOpenChange={setSaveOpen} onSave={(name) => { setSaved([ ...saved, { id: nextId.current++, name, selection: applied }, ]); }} > <SelectionChips selection={applied} /> </FilterSaveDialog> </> )}
<FilterSaveDialog open={editing !== null} onOpenChange={(isOpen) => { if (!isOpen) setEditing(null); }} onSave={(name) => { setSaved( saved.map((f) => (f.id === editing?.id ? { ...f, name } : f)), ); }} title="Edit Filter" submitLabel="Edit Filter" defaultName={editing?.name} > {editing && <SelectionChips selection={editing.selection} />} </FilterSaveDialog> </div> );}Accessibility
Section titled “Accessibility”The icon-only actions — FilterChipRemove, FilterSavedItemEdit,
FilterSavedItemDelete — have no accessible name of their own; give each an
aria-label naming the filter it acts on.
Filter
Section titled “Filter”Stateless filtering surface: a trigger button, a two-column popup (category nav plus a free-form pane), and applied-filter chips. Selection, apply, clear, and saved filter sets live in the consumer. Groups the filter’s parts without rendering an HTML element; accepts Popover props.
children
Section titled “children”children?: React.ReactNodeThe popover’s parts.
defaultOpen
Section titled “defaultOpen”defaultOpen?: booleanWhether the popover is initially open. Defaults to false.
defaultTriggerId
Section titled “defaultTriggerId”defaultTriggerId?: string | nullID of the trigger associated with an initially open popover.
modal?: boolean | "trap-focus"Whether opening the popover limits interaction outside it. Defaults to
false.
onOpenChange
Section titled “onOpenChange”onOpenChange?: ((open: boolean, eventDetails: PopoverPrimitive.Root.ChangeEventDetails) => void)Event handler called when the popover is opened or closed.
onOpenChangeComplete
Section titled “onOpenChangeComplete”onOpenChangeComplete?: ((open: boolean) => void)Event handler called after popover animations complete.
open?: booleanWhether the popover is currently open.
triggerId
Section titled “triggerId”triggerId?: string | nullID of the trigger associated with a controlled popover.
FilterApply
Section titled “FilterApply”Commits the draft selection: wire onClick to apply and close the popup.
count is the number of categories with a selection and shows in the label
while positive. On mobile it also returns the two-screen layout to the nav —
the built-in instance inside FilterPaneFooter does only that, leaving the
commit to FilterNavFooter. Renders a Button and accepts standard Button
props.
className
Section titled “className”className?: stringCSS class applied to the element.
count?: number = 0focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null = "xs"Controls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | nullControls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterButton
Section titled “FilterButton”Opens the filter popup. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullControls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | null = "outline"Controls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterChip
Section titled “FilterChip”With category and values, renders the canonical label — category: value
for a single value, category: N options past one — before the children (the
remove action). Renders a Badge and accepts standard Badge props.
category
Section titled “category”category?: stringrender
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
values
Section titled “values”values?: string[]variant
Section titled “variant”variant?: "outline" | "success" | "info" | "link" | "secondary" | "default" | "ghost" | "destructive" | "attention" | "highlight" | null = "secondary"Controls the badge’s appearance only, never its behavior. The link
variant looks like a link without rendering one, and status variants such
as success carry no accessible semantics, so the badge’s text has to
convey the meaning on its own.
FilterChipRemove
Section titled “FilterChipRemove”Remove action for a chip. Give it an aria-label naming the filter it
removes. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullControls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | nullControls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterChips
Section titled “FilterChips”Groups the applied-filter chips. Renders a <div> element and accepts
standard <div> props.
FilterClearAll
Section titled “FilterClearAll”Clears the whole draft selection via the consumer’s onClick. Renders a
Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null = "xs"Controls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | null = "ghost"Controls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterContent
Section titled “FilterContent”Includes a portal and the positioned popup. Renders a <div> element by
default and accepts standard <div> props. Below the sm breakpoint it
becomes a two-screen flow: the category nav first, then the open pane in its
place.
align?: Align = "start"className
Section titled “className”className?: string | ((state: PopoverPopupState) => string | undefined)finalFocus
Section titled “finalFocus”finalFocus?: boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => void | boolean | HTMLElement | null)initialFocus
Section titled “initialFocus”initialFocus?: boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => void | boolean | HTMLElement | null)render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverPopupState>style?: React.CSSProperties | ((state: PopoverPopupState) => React.CSSProperties | undefined)FilterFooter
Section titled “FilterFooter”Groups a pane’s clear and apply actions. Renders a <div> element and
accepts standard <div> props.
FilterHeader
Section titled “FilterHeader”Mobile-only bar with the popup title and a close action; put it first inside
FilterContent. Renders a <div> element and accepts standard <div>
props.
FilterNav
Section titled “FilterNav”Lists the filter categories. Renders a <div> element and accepts standard
<div> props.
FilterNavFooter
Section titled “FilterNavFooter”Mobile-only footer for the category list. Render once alongside FilterNav,
unconditionally — it hides itself once a pane is open, so it doesn’t need
gating on the open category like FilterPane’s own footer. Renders a
FilterFooter and accepts FilterFooter props.
FilterNavItem
Section titled “FilterNavItem”Opens its category’s pane. Renders a <button> element and accepts standard
<button> props.
active
Section titled “active”active?: booleanMarks the item as the currently open category.
applied
Section titled “applied”applied?: booleanMarks the category as having selections.
values
Section titled “values”values?: string[]The selected values, shown under the label on mobile — the single value, or a count past one.
FilterNavSaved
Section titled “FilterNavSaved”Entry to the saved filter sets; when present, put it first inside
FilterNav. Renders a FilterNavItem and accepts FilterNavItem props.
active
Section titled “active”active?: booleanMarks the item as the currently open category.
applied
Section titled “applied”applied?: booleanMarks the category as having selections.
values
Section titled “values”values?: string[]The selected values, shown under the label on mobile — the single value, or a count past one.
FilterOption
Section titled “FilterOption”Row that toggles the Checkbox or RadioGroupItem placed inside it. Renders a
<label> element and accepts standard <label> props.
FilterOptionList
Section titled “FilterOptionList”Scrollable pane body for FilterOption rows; free-form controls use
FilterPaneContent. Renders a <div> element and accepts standard <div>
props.
FilterPane
Section titled “FilterPane”Contains one category’s controls. Renders a <div> element and accepts
standard <div> props.
FilterPaneBack
Section titled “FilterPaneBack”Mobile-only back action for a pane header; returns to the category list on its own. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullControls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | nullControls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterPaneClear
Section titled “FilterPaneClear”Clears the open category; render it only while the category has a selection. Hidden on mobile, where the pane footer’s Clear takes over. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null = "xs"Controls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | null = "ghost"Controls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterPaneContent
Section titled “FilterPaneContent”Scrollable pane body for free-form content; option lists use
FilterOptionList. Renders a <div> element and accepts standard <div>
props.
FilterPaneFooter
Section titled “FilterPaneFooter”Mobile-only footer for a specific pane. Render it alongside a pane’s own
FilterFooter — the pane hides that one on mobile while this is present.
This one clears just that category via onClear, and its FilterApply
returns to the category nav without applying anything; committing the whole
draft happens once, in FilterNavFooter. Renders a FilterFooter and accepts
FilterFooter props except children.
clearLabel
Section titled “clearLabel”clearLabel?: React.ReactNodeonClear
Section titled “onClear”onClear: () => voidFilterPaneHeader
Section titled “FilterPaneHeader”Groups a pane’s title with its back and clear actions. Renders a <div>
element and accepts standard <div> props.
FilterPaneTitle
Section titled “FilterPaneTitle”Provides the pane’s title. Renders a <div> element and accepts standard
<div> props.
FilterSave
Section titled “FilterSave”Save action rendered next to the applied-filter chips. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null = "xs"Controls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | null = "ghost"Controls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterSaveDialog
Section titled “FilterSaveDialog”Names a filter set. Calls onSave with the trimmed name and closes;
persisting the set lives in the consumer. Children render between the name
field and the footer — pass FilterChips previewing the set. Reuse the same
component to edit a saved filter’s name: pass defaultName, and override
title/submitLabel (e.g. both "Edit Filter"). Accepts Dialog props.
children
Section titled “children”children?: React.ReactNodedefaultName
Section titled “defaultName”defaultName?: stringdefaultOpen
Section titled “defaultOpen”defaultOpen?: booleanWhether the dialog is initially open. To render a controlled dialog, use
the open prop instead.
defaultTriggerId
Section titled “defaultTriggerId”defaultTriggerId?: string | nullID of the trigger associated with an initially open dialog.
description
Section titled “description”description?: string = "Name this filter set to reuse it from the filter menu."disablePointerDismissal
Section titled “disablePointerDismissal”disablePointerDismissal?: booleanWhether to prevent the dialog from closing on outside presses.
modal?: boolean | "trap-focus"Determines if the dialog enters a modal state when open. true traps
focus, locks page scroll, and disables pointer interactions outside the
dialog. false allows interaction with the rest of the document.
'trap-focus' traps focus without locking scroll or disabling outside
pointer interactions.
onOpenChange
Section titled “onOpenChange”onOpenChange?: ((open: boolean, eventDetails: DialogRoot.ChangeEventDetails) => void)Event handler called when the dialog is opened or closed.
onOpenChangeComplete
Section titled “onOpenChangeComplete”onOpenChangeComplete?: ((open: boolean) => void)Event handler called after animations complete when the dialog is opened or closed.
onSave
Section titled “onSave”onSave: (name: string) => voidopen?: booleanWhether the dialog is currently open.
submitLabel
Section titled “submitLabel”submitLabel?: string = "Save Filter"title?: string = "Save Filter"triggerId
Section titled “triggerId”triggerId?: string | nullID of the trigger associated with a controlled dialog.
FilterSavedItem
Section titled “FilterSavedItem”Row for one saved filter set. Renders a <div> element and accepts standard
<div> props.
FilterSavedItemButton
Section titled “FilterSavedItemButton”Applies the saved set on click; the consumer owns what “apply” means. Renders
a <button> element and accepts standard <button> props.
FilterSavedItemDelete
Section titled “FilterSavedItemDelete”Delete action for a saved filter; give it an aria-label naming the set it
deletes. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullControls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | nullControls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterSavedItemEdit
Section titled “FilterSavedItemEdit”Edit action for a saved filter; give it an aria-label naming the set it
edits. Renders a Button and accepts standard Button props.
className
Section titled “className”className?: stringCSS class applied to the element.
focusableWhenDisabled
Section titled “focusableWhenDisabled”focusableWhenDisabled?: booleanWhether a disabled button stays focusable, so keyboard and screen reader users can still reach it and read its label.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether the element passed to render is a native button. Set to false
when rendering another element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a React element or a function that returns the element to render.
size?: "xs" | "sm" | "lg" | "default" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullControls the button’s size. The icon sizes are square and are for buttons whose only child is an icon.
style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component’s state.
variant
Section titled “variant”variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | nullControls the button’s appearance only, never its behavior. The link
variant looks like a link without rendering one.
FilterSearch
Section titled “FilterSearch”Search field for narrowing a long option list. Renders an Input and accepts
standard <input> props.