Filter menu
A filter button whose menu lists the fields you can filter by, then each field's values. Start typing and the same menu becomes a search across every field and value, like a command menu.
Features
- Two levels: pick a field such as Status, then tick any number of its values. Each field shows how many of its values are on.
- Typing turns the menu into a search, grouped by field, with the matching letters in bold. Inside a field, the search stays within it.
- Or, with
search="button", a Search filters button takes the place of the search box, and the menu slides over to its search state when it's used. - Escape steps back: first it clears the search, then it leaves the field, and only then does it close the menu.
- Views slide forward and back, and the popup's height follows its content. Under reduced motion, views swap in place.
Installation
npx shadcn@latest add https://fibo.toribryan.com/r/filter-menu.jsonimport { FilterMenu, type FilterField, type FilterValue } from "@/components/ui/filter-menu"It installs the Button component and the motion package too.
Usage
import { useState } from "react"
import {
FilterMenu,
type FilterField,
type FilterValue,
} from "@/components/ui/filter-menu"
const fields: FilterField[] = [
{
id: "status",
label: "Status",
options: [
{ value: "todo", label: "Todo" },
{ value: "done", label: "Done" },
],
},
{
id: "priority",
label: "Priority",
options: [
{ value: "urgent", label: "Urgent" },
{ value: "low", label: "Low" },
],
},
]
export function IssueFilters() {
const [filters, setFilters] = useState<FilterValue>({})
return (
<FilterMenu fields={fields} value={filters} onValueChange={setFilters} />
)
}The menu only picks filters. value holds the chosen values keyed by field
id, such as { status: ["todo"] }, and showing the applied filters is up to
your app.
type FilterField = {
id: string
label: string
icon?: React.ReactNode
options: { value: string; label: string; icon?: React.ReactNode }[]
}
type FilterValue = Record<string, string[]>Anatomy
| Part | data-slot | Notes |
|---|---|---|
| FilterMenu | Renders the trigger, and the popup while it's open. | |
| trigger | filter-menu-trigger | The Filter button. |
| popup | filter-menu | In a portal on the body. Carries data-view. |
| search | filter-menu-search | The header: the search box, or a field's name, with a back button. |
| search button | filter-menu-search-button | With search="button", on the field menu. |
| group label | filter-menu-group-label | A field's name above its results, in a search. |
| field | filter-menu-field | A field in the menu, with its count. |
| option | filter-menu-option | A value, with a checkbox. |
Guidelines
- 01Use it above a list or table that people narrow down by several fields, like issues, orders or files.
- 02Keep field names to one or two words, and order fields by how often people use them.
- 03Order each field's values the way people think of them: a workflow in its order, people by name, sizes from small to large.
- 04Show the applied filters next to the button, such as chips people can remove, so the list never looks mysteriously short.
- 05For a single yes-or-no filter, a switch or a checkbox beside the list is quicker.
Examples
Applied as chips
The component only picks filters. Here the app shows each applied field as a chip beside the button, with a button to remove it.
Search button
search="button" swaps the search box for a Search filters button. Using it
slides the whole menu, header and body, over to the search state, which
lists every value until you type. Back, Escape or Backspace slides it home.
Typing a letter on the menu starts a search too.
Default
The menu on its own, keeping its own state.
With selections
defaultValue starts with values chosen. The fields that have any show a
count. The trigger's content is yours, here with a dot to say filters are on.
Keyboard only
Opened from the keyboard: arrows move, Right opens a field, Enter ticks a value, and Left or Backspace goes back to the field it came from.
No matches
A search with no results says so, using emptyText.
Do's and don'ts
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
fields | FilterField[] | The fields people can filter by, each with the values it offers. | |
value | FilterValue | The chosen values, keyed by field id, when you control the state. | |
defaultValue | FilterValue | {} | The chosen values to start with, when the menu keeps its own state. |
onValueChange | (value: FilterValue) => void | Called with every field's chosen values each time one is toggled. | |
triggerLabel | ReactNode | "Filter" | What the trigger button says. |
placeholder | string | "Filter by…" | Hint in the inline search box while the field menu is showing. |
emptyText | ReactNode | "No matching filters" | Shown when a search matches nothing. |
label | string | "Filters" | Names the popup for assistive technology. |
searchLabel | string | "Search filters" | Names the search box for assistive technology. |
align | "start" | "center" | "end" | "start" | Which edge of the trigger the popup lines up with. |
search | "inline" | "button" | "inline" | How search starts. inline keeps a search box at the top, and typing turns the menu into results. button shows a Search filters button instead, and the menu slides over to its search state when it's used. |
className | string | Classes for the trigger button. |
Data attributes
| Attribute | Element | Present when |
|---|---|---|
data-slot="filter-menu-trigger" | The trigger | Always. |
data-slot="filter-menu" | The popup | While it's open. |
data-view | The popup | While it's open. fields, values or search. |
data-slot="filter-menu-search" | The search row | Always. |
data-slot="filter-menu-search-button" | The Search filters button | search="button", on the field menu. |
data-search | The popup | While it's open. inline or button. |
data-slot="filter-menu-group-label" | A field's name in search results | Searching from the field menu. |
data-slot="filter-menu-field" | A field row | Showing the field menu. |
data-slot="filter-menu-option" | A value row | Showing values or search results. |
data-highlighted | A field or value row | The keyboard or pointer is on it. |
data-checked | A value row | That value is chosen. |
Accessibility
- 01Focus goes to the search box when the menu opens, and stays there. The box is a combobox, and aria-activedescendant points to the highlighted row, so screen readers read each row as the arrows move.
- 02With the search button, the list takes focus instead and points at the highlighted row itself, and the button is one Tab away. Entering search moves focus into the search box; leaving it moves focus back to the list.
- 03Values are options in a multi-select listbox, announced as selected or not. Each field is read with how many of its values are chosen and that it opens its values.
- 04A search announces how many results it found.
- 05Escape steps back one level at a time, and the last press returns focus to the Filter button.
- 06Rename every piece of text for your product's language: the trigger with
triggerLabel, the popup withlabel, and the search box withsearchLabelandplaceholder.
Related components
- SelectIn fibo's Storybook
- CheckboxIn fibo's Storybook
- Chapter scrubberA rail of marks that swell under the pointer like the Dock, previewing the chapter at the crest.
References
- Base UI Popover, for the popup
- WAI-ARIA combobox pattern
- Motion for React, for the view transitions
