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.

StatusTodo, In progressPriorityUrgent

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

Installnpx shadcn@latest add https://fibo.toribryan.com/r/filter-menu.json
Importimport { 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

Partdata-slotNotes
FilterMenuRenders the trigger, and the popup while it's open.
triggerfilter-menu-triggerThe Filter button.
popupfilter-menuIn a portal on the body. Carries data-view.
searchfilter-menu-searchThe header: the search box, or a field's name, with a back button.
search buttonfilter-menu-search-buttonWith search="button", on the field menu.
group labelfilter-menu-group-labelA field's name above its results, in a search.
fieldfilter-menu-fieldA field in the menu, with its count.
optionfilter-menu-optionA 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.

StatusTodo, In progressPriorityUrgent

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

StatusBacklog, Todo, In progress, Done
DoGroup values under a short field name, in the order people expect.
Issue status filter optionsDone, Backlog, In progress, Todo
Don'tDon't write long field names or shuffle values. The menu is for scanning.

API reference

PropTypeDefaultDescription
fieldsFilterField[]The fields people can filter by, each with the values it offers.
valueFilterValueThe chosen values, keyed by field id, when you control the state.
defaultValueFilterValue{}The chosen values to start with, when the menu keeps its own state.
onValueChange(value: FilterValue) => voidCalled with every field's chosen values each time one is toggled.
triggerLabelReactNode"Filter"What the trigger button says.
placeholderstring"Filter by…"Hint in the inline search box while the field menu is showing.
emptyTextReactNode"No matching filters"Shown when a search matches nothing.
labelstring"Filters"Names the popup for assistive technology.
searchLabelstring"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.
classNamestringClasses for the trigger button.

Data attributes

AttributeElementPresent when
data-slot="filter-menu-trigger"The triggerAlways.
data-slot="filter-menu"The popupWhile it's open.
data-viewThe popupWhile it's open. fields, values or search.
data-slot="filter-menu-search"The search rowAlways.
data-slot="filter-menu-search-button"The Search filters buttonsearch="button", on the field menu.
data-searchThe popupWhile it's open. inline or button.
data-slot="filter-menu-group-label"A field's name in search resultsSearching from the field menu.
data-slot="filter-menu-field"A field rowShowing the field menu.
data-slot="filter-menu-option"A value rowShowing values or search results.
data-highlightedA field or value rowThe keyboard or pointer is on it.
data-checkedA value rowThat 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 with label, and the search box with searchLabel and placeholder.

References