Skip to main content

FilterChipGroup

Implemented

Filter 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.

Categorymolecules
Sourcepatterns/molecules/filterchipgroup.html
AI HintToolbar facets, A–Z letter filters and any single- or multi-select filter row. Composes the Chip atom. For static categorization display use CategoryPill (not a control). Wire ariaLabel from your i18n keys — the EN default leaks on non-EN pages otherwise.

Row 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 the Chip atom — Enter/Space activation and tab focus come free, with a visible focus-visible ring.
  • 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's data-selected-count attribute 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-body with dark overrides and a --ep-teal hover border.

i18n status

Honest state as of 2026-08-28 — the molecule renders English fallbacks and has no formatter props yet:

  • The ariaLabel default "Filters" is an English fallback. Pass a localized label at every call site (t("ranges.ariaLabel"), t("alphaAriaLabel") — the established key families).
  • The showCount plural suffix ("result"/"results") is English-only with no escape hatch — a latent leak: no storefront consumer passes showCount today. A countLabel formatter prop is the planned fix.
  • Pending consumer leaks (cascade step 6, not yet fixed): ProducerProfilesTable passes a literal "A-Z filter" (and is also missing multi={false}); TableToolbar exposes filtersLabel (landed, molecules 31-45) — its 4 filter-using consumers pass localized labels.

Props

Prop
Type
Default
Description
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[]) => void
—

Fires 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).

multi
boolean
true

true: 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.

showCount
boolean
false

Folds 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.

className
string
—

Additional classes appended to the group container (base: flex flex-wrap gap-2). TableToolbar passes flex-nowrap overflow-x-auto for its dense row.

ariaLabel
string
Filters

Accessible name for the role="group" container. Always pass a localized label — the English "Filters" default is a fallback, not a translation.

size
'sm' | 'md'
md

Chip size passed through to the Chip atom. "sm" (px-2, text-ep-xs) matches the TableToolbar rhythm; "md" suits standalone filter rows.

Props

Preview

FilterChipGroup

Prop
Type
Required
Default
Description
options
FilterChipOption[]
Required
—

Chip descriptors — { id, label, count? } (FilterChipOption).

selectedIds
string[]
Required
—

Currently selected option ids (controlled).

onChange
(selectedIds: string[]) => void
Required
—

Emits the full selection — multiple ids in multi mode, a single id in single-select mode.

multi
boolean
Optional
true

true = toggle many chips (aria-pressed buttons); false = radio-like single select (A–Z filters).

showCount
boolean
Optional
false

Render option counts inside the chip; SR announces ‘{label}, {count} result(s)’.

className
string
Optional
—

Additional CSS classes on the wrapping row.

ariaLabel
string
Optional
'Filters'

Accessible name for the role='group' row — pass a localized string on non-EN pages (EN default).

size
'sm' | 'md'
Optional
'md'

Chip size passed through to the Chip atom — 'sm' matches the TableToolbar h-7 rhythm.

Quick use:<FilterChipGroup />

Accessibility

ARIA rolegroup

ARIA

1 rule

WCAG 4.1.2

row is role='group' with aria-label; each chip is a <button aria-pressed>; count text is announced (single/plural)

Type

1 rule

NASA Rule 5

single-select (multi={false}) still emits arrays — keep exactly one id in state

Usage

Import

tsxSnippet
import { FilterChipGroup } from "@epasslive/design-system"

Example 1

tsxSnippet
<FilterChipGroup options={STATUS_OPTIONS} selectedIds={sel} onChange={setSel} ariaLabel="Filter by status" />

Example 2

tsxSnippet
<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

Design System v4.1.0 | ePassLive