FilterChipGroup
ImplementedFilter row composing the Chip atom. Active chips use the canonical DS selected-control idiom (theme-stable 5.70:1 contrast). Supports single- and multi-select modes; optional count badges are announced by SR.
patterns/molecules/filterchipgroup.htmlRow of toggleable filter chips composing the Chip atom. Additive multi-select by default; multi={false} switches to radio semantics for exactly-one filters (date ranges, A–Z indexes). Supersedes CategoryPill.
FilterChipGroup
Filter molecule — Navigation Molecules, Family 5
Controlled chip group: the parent owns selectedIds and reacts to onChange. The container exposes role="group" with an accessible name; every chip is a native aria-pressed button from the Chip atom. Prefer it over hand-rolled pill rows — and over CategoryPill, which it supersedes.
Multi-select with counts (default size, showCount)
Selected: concerts, festivals — counts are announced to screen readers, not rendered visually
Single-select (multi={false}) — clicking the active chip is a no-op
Selected: week
Compact toolbar row (size="sm")
A–Z index, single-select in a narrow container (wraps)
This component supports light and dark mode.
Usage
FilterChipGroup is a controlled component — the parent owns selectedIds and reacts to onChange. Pick the mode by shape of the filter: several facets at once (categories) uses the default multi; exactly-one filters (date ranges, A–Z indexes) pass multi={false}. Always pass a localized ariaLabel.
import { useState } from 'react'
import { FilterChipGroup } from '@epasslive/design-system'
const RANGES = [
{ id: 'today', label: 'Today' },
{ id: 'week', label: 'This week' },
{ id: 'month', label: 'This month' },
{ id: 'all', label: 'All time' },
]
const [range, setRange] = useState(['week'])
// Single-select date range (radio semantics — RevenueReport's shape):
<FilterChipGroup
options={RANGES}
selectedIds={range}
onChange={setRange}
multi={false}
ariaLabel={t('ranges.ariaLabel')}
/>
// Multi-select category facets (additive toggle):
const [categories, setCategories] = useState(['concerts', 'festivals'])
<FilterChipGroup
options={CATEGORIES}
selectedIds={categories}
onChange={setCategories}
ariaLabel={t('categoriesAriaLabel')}
/>Accessibility
- Container exposes
role="group"with an accessible name (ariaLabel); each chip is a native<button type="button" aria-pressed>rendered by theChipatom — Enter/Space activation and tab focus come free, with a visiblefocus-visiblering. - Arrow-key navigation is deliberately not implemented: the group follows the WAI-ARIA APG toggle-buttons-in-a-group pattern (not listbox/toolbar), where roving tabindex is not required. Chips wrap with
flex flex-wrap gap-2; long rows (A–Z) are tabbed through. - With
showCount, the count folds into the chip's accessible name ("Festivals, 15 results"). The container'sdata-selected-countattribute is for styling and tests — it is not announced. - Tap targets are ≥44px tall (
--ep-tap-min) in both sizes (WCAG 2.5.8 AA passes with room). Known gap: single-letter chips can be narrower than 44px — a WCAG 2.5.5 AAA width item tracked on the Chip atom. - Selected chips use the theme-stable selected-control idiom (
--ep-btn-primary-bg+--ep-text-inverse, 5.70:1) verified in light and dark themes; unselected chips use--ep-bg-3/--ep-border/--ep-text-bodywith dark overrides and a--ep-tealhover border.
i18n status
Honest state as of 2026-08-28 — the molecule renders English fallbacks and has no formatter props yet:
- The
ariaLabeldefault"Filters"is an English fallback. Pass a localized label at every call site (t("ranges.ariaLabel"),t("alphaAriaLabel")— the established key families). - The
showCountplural suffix ("result"/"results") is English-only with no escape hatch — a latent leak: no storefront consumer passesshowCounttoday. AcountLabelformatter prop is the planned fix. - Pending consumer leaks (cascade step 6, not yet fixed):
ProducerProfilesTablepasses a literal"A-Z filter"(and is also missingmulti={false});TableToolbarexposesfiltersLabel(landed, molecules 31-45) — its 4 filter-using consumers pass localized labels.
Props
options*FilterChipOption[]Filter options. Each has id, label, an optional count (announced when showCount) and an optional leading icon (rendered aria-hidden). No per-option disabled flag yet — the Chip atom supports it but FilterChipOption does not expose it.
selectedIds*string[]Controlled list of selected option ids. The parent owns the selection state.
onChange*(selectedIds: string[]) => voidFires with the full next selection: toggled id added/removed in multi mode, [id] on replacement in single mode. Never fires when single-select clicks the already-active chip (no-op).
multibooleantruetrue: additive toggle — clicking a selected chip deselects it. false: radio semantics — clicking an inactive chip replaces the selection. No built-in clear: include an "All" option if consumers need one.
showCountbooleanfalseFolds opt.count into the chip's accessible name ("Festivals, 15 results") so screen readers announce it. The count is NOT rendered visually. English-only plural today — see the i18n section.
classNamestringAdditional classes appended to the group container (base: flex flex-wrap gap-2). TableToolbar passes flex-nowrap overflow-x-auto for its dense row.
ariaLabelstringFiltersAccessible name for the role="group" container. Always pass a localized label — the English "Filters" default is a fallback, not a translation.
size'sm' | 'md'mdChip size passed through to the Chip atom. "sm" (px-2, text-ep-xs) matches the TableToolbar rhythm; "md" suits standalone filter rows.
Props
FilterChipGroup
optionsFilterChipOption[]—Chip descriptors — { id, label, count? } (FilterChipOption).
selectedIdsstring[]—Currently selected option ids (controlled).
onChange(selectedIds: string[]) => void—Emits the full selection — multiple ids in multi mode, a single id in single-select mode.
multibooleantruetrue = toggle many chips (aria-pressed buttons); false = radio-like single select (A–Z filters).
showCountbooleanfalseRender option counts inside the chip; SR announces ‘{label}, {count} result(s)’.
classNamestring—Additional CSS classes on the wrapping row.
ariaLabelstring'Filters'Accessible name for the role='group' row — pass a localized string on non-EN pages (EN default).
size'sm' | 'md''md'Chip size passed through to the Chip atom — 'sm' matches the TableToolbar h-7 rhythm.
<FilterChipGroup />Accessibility
ARIA
1 ruleType
1 ruleUsage
Import
import { FilterChipGroup } from "@epasslive/design-system"Example 1
<FilterChipGroup options={STATUS_OPTIONS} selectedIds={sel} onChange={setSel} ariaLabel="Filter by status" />Example 2
<FilterChipGroup options={LETTERS} selectedIds={sel} onChange={(ids) => setSel(ids.slice(-1))} multi={false} ariaLabel="Filter by first letter" />Anti-patterns
— Do not omit multi={false} on single-select A–Z rows — default multi accumulates pressed chips
— Do not hand-roll aria-pressed chip rows — the count announcement and idiom contrast live here