Skip to content
hightouchUI

Design system

d400872

Filter multi select

Filter multi select is a filter trigger that opens a popover of checkbox options and summarizes the current selection ("All" or a count) on the trigger itself.

Its default trigger variant matches multi select, so it lines up with form fields such as a search input that are also used as filters. The pill variant's shape works best when one or more FilterMultiSelects are the only filter mechanisms above a table or list.

Example

FilterMultiSelect is the batteries-included form: one trigger, one checkbox list, an All/count summary.

Usage

Selection and the "All" row

FilterMultiSelect's trigger reads "All" for a full selection and a count for a partial one; an empty selection shows no summary at all, leaving the trigger as its bare label, so an emptied filter never looks like "All" while the filtered table is empty. The "All" row matches: checked for a full selection, mixed for a partial one, unchecked for an empty one. It toggles — selecting everything, and clearing everything from a full selection.

A caller that treats "nothing selected" as "no filter" should seed value with every option, so the trigger reads "All" at rest.

When nothing checked means no filter (values opt in, and checking every value still excludes items without the attribute), pass showAllRow={false}. An "All" row would read as a filter of its own, and the trigger counts a full selection instead of saying "All".

Pill variant

Pass variant="pill" to give FilterMultiSelect a rounded FilterPill trigger that works best when only paired with other FilterMultiSelects, e.g. in a row of filters above a table.

Width

FilterMultiSelect sizes to its content. Pass width to line it up with the fields around it, such as 100% to fill a column; a long label then truncates, with the full label in a tooltip, while the summary stays whole. The pill variant always sizes to its content and takes no width.

Badges

Give a FilterPillOption a badge for short text after its label, such as a count.

Long lists

Past 25 options, the list adds a search box, which takes focus when the popover opens, and caps its height. Only the rows in view render, so a list of hundreds of people opens as fast as a short one. While a search narrows the list, the "All" row hides, since it would also check the options the search filters out. Nothing changes for the caller.

Several lists in one popover

FilterOptionList renders the checkbox list without its own popover, so one FilterPill (or any popover) can hold several filters. Each list's label renders as its section header.

Many filters in a narrow space

Where a row of pills doesn't fit, FilterSectionList puts every filter behind one pill. It lists the filters by name with their selected counts, and opens one at a time, so a long list keeps its search and scrolling instead of stacking under the others.

Leave each section's FilterOptionList without a label: the section's back row already names it.

A popover the pill can't host

FilterPill owns its popover: uncontrolled open state, a 240px panel, and focus contained to the option list. A filter whose panel disagrees with any of that (a controlled open, an auto-width editor, a cascade) uses FilterPillButton as the trigger of its own popover, so it still reads as the same pill.

PopoverTrigger injects a ref, so the trigger must be FilterPillButton itself, not a wrapper around it.

Stale values

FilterMultiSelect ignores selected values that are no longer in options, so a deleted option can't make the trigger say "All" while the popover disagrees.

Guidelines

When to use

  • When a table or list is filtered by one attribute with a small set of checkbox options.
  • When the filter sits among form fields also used to filter, such as beside a search input, in the default variant so its trigger matches them.
  • When several filters sit in a row above the thing they filter and should read as applied filters, use the pill variant.

When not to use

  • If the user picks values to save, such as in a form, use a multi select.
  • If the dropdown triggers one-shot actions rather than holding a selection, use a menu.
  • If the panel should be a menu rather than a checkbox list with an "All" row, use a filter menu.
  • For free-text entry, or to type and pick a single value, use a combobox.

Content

Accessibility

Opening the trigger focuses the first option's checkbox, or a long list's search box, and arrow keys move between the row checkboxes — across every list when several share one popover. In a long list they step from the search box through the "All" row and each option in turn, scrolling the next one into view, and stay within that list. While a search narrows the list, "All" hides and arrows step from the search box to the matches. Tab reaches the search box, the "All" row when it shows, and each checkbox and "Only" button in view, and stays inside the panel; a long list renders only the rows in view, so arrow keys reach the rest. Escape closes the panel and returns focus to the trigger.

In a FilterSectionList, opening the trigger focuses the first section row, and arrow keys move between the rows. Opening a section focuses its search box or first checkbox, and its options are grouped under the section's name. The back row returns to the sections and refocuses the one just left.

The trigger is named by its label and summary followed by ", press to show options", like a multi select's, such as "Channels All, press to show options". The pill variant is named by its label and summary alone, such as "Channels All". Neither needs a FormField or HiddenFormLabel. An option's badge is not part of its checkbox's name.

Clicking anywhere on a row toggles that option, the same as its checkbox; clicking the option's text also moves focus to that checkbox, so the arrow keys carry on from the option just clicked. "Only" narrows the selection to that option alone, and is the keyboard and screen-reader path to doing so. The row itself is deliberately not a control: it holds a checkbox and a button, and giving it a role of its own would nest interactive elements.

Props

FilterMultiSelect

A filter trigger with a single checkbox FilterOptionList and an All/count summary: select-style by default, or a FilterPill with variant="pill". Reach for this first; drop to the parts below only to compose several lists in one popover.

NameDefaultDescription
label

—

stringFilter name shown on the trigger and above the option list.
value

—

Set<T>Selected option values.
onChange

—

(value: Set<T>) => voidCallback with the next selection when the user toggles options.
options

—

Omit<FilterPillOption<T>, "icon">[]Selectable options. Their rows show no icon.
showOnlyAffordancetruebooleanReveals a "Only" affordance on each row that narrows the selection to it.
showAllRowtruebooleanRenders the "All" row that checks or clears every option. Turn it off for a list where nothing checked means no filter, since there "All" would read as a filter of its own.
variant"default""default" | "pill"Trigger appearance. "default" matches MultiSelect, and "pill" is the rounded filter pill.
width"fit-content""4xs" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "auto" | "fit-content" | "100%"Trigger width. A pill sizes to its content and takes no width.

FilterPill

The pill trigger and its popover shell. summary renders next to the label; children is the popover body.

NameDefaultDescription
children

—

ReactNodePopover contents, typically one or more FilterOptionLists or a FilterSectionList.
placement"bottom-start""bottom-start" | "bottom-end"Popover placement relative to the trigger.
label

—

stringFilter name shown on the pill.
summary

—

ReactNodeCurrent-selection summary rendered next to the label (e.g. "All" or a count badge).
isLoading

—

booleanToggles the loading state.
isDisabled

—

booleanToggles the disabled state.
isJustified

—

booleanTake up an entire available container width.
size

—

"sm" | "md" | "lg" | "xl"Determines the height of the button.

FilterPillButton

The pill trigger on its own, for a popover FilterPill can't host. Unlike FilterPill it accepts icon, for a pill that carries a leading glyph.

NameDefaultDescription
label

—

stringFilter name shown on the pill.
summary

—

ReactNodeCurrent-selection summary rendered next to the label (e.g. "All" or a count badge).
isLoading

—

booleanToggles the loading state.
isDisabled

—

booleanToggles the disabled state.
isJustified

—

booleanTake up an entire available container width.
size

—

"sm" | "md" | "lg" | "xl"Determines the height of the button.
icon

—

ComponentType<SVGAttributes<SVGElement>>Primary icon that's displayed on the left. Only icon or imageUrl can be set at the same time.

FilterOptionList

The checkbox list (with "All" and a hover "Only" affordance) without a popover, so several filters can share one. showAllRow={false} drops the "All" row for opt-in lists.

NameDefaultDescription
label

—

stringRendered as a header above the list when provided.
value

—

Set<T>Selected option values.
onChange

—

(value: Set<T>) => voidCallback with the next selection when the user toggles options.
options

—

FilterPillOption<T>[]Selectable options.
showOnlyAffordancetruebooleanReveals a "Only" affordance on each row that narrows the selection to it.
showAllRowtruebooleanRenders the "All" row that checks or clears every option. Turn it off for a list where nothing checked means no filter, since there "All" would read as a filter of its own.

FilterSectionList

Several filters behind one trigger, listed by name and opened one at a time.

NameDefaultDescription
sections

—

FilterSection[]The filters, listed in order.
footer

—

ReactNodeShown below the section rows, such as a button that clears every filter.

FilterSection

One filter in a FilterSectionList.

NameDefaultDescription
key

—

stringIdentifies the section among its siblings.
label

—

stringSection name, shown on its row and above its options.
selectedCount

—

numberHow many of the section's values are selected. A count above zero shows in a badge on its row.
content

—

ReactNodeThe section's options, typically an unlabeled FilterOptionList.

FilterPillOption

One option in a FilterMultiSelect or FilterOptionList.

NameDefaultDescription
label

—

stringOption text shown on the row.
value

—

TValue written into the selection set.
icon

—

FC<{ size: number }>Leading icon shown before the label in a FilterOptionList. FilterMultiSelect shows no option icons.
badge

—

stringShort text, such as a count, shown in a badge after the label.