CategoryFilter
Filter pill row for list pages (today the work index; the blog archive uses BlogCategoryFilter). Multiple visual treatments (pills, underline, minimal) for different surface densities.
Status: stable. Group: navigation. Markdown version: /design-system/components/category-filter.md
When to use
Pill-row filter for list pages: one selectable category set with visual treatments per surface (today the work index; the blog archive uses BlogCategoryFilter). Selection state lives in the parent.
When not to use
- Do not use as primary navigation;
CategoryFilteris a UI control, not a nav surface; useNavLinkfor routes. - Do not stack two filter rows when one composite control would do; readability collapses.
Look-alikes
- BlogCategoryFilter: Do not use this for generic list pages; that is CategoryFilter.
- NavLink: Do not use as primary navigation;
CategoryFilteris a UI control, not a nav surface; useNavLinkfor routes.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
categories | CategoryOption[] | none | Array of category options. | Yes |
activeCategory | string | none | Currently active category value (single mode) | No |
onCategoryChange | (category: string) => void | none | Callback when category changes (single mode) | No |
multiple | boolean | false | Lets several chips stay pressed at once. Pressing a chip toggles it, pressing the allValue chip clears the selection, and the collapsible row stays open while picking. | No |
activeCategories | string[] | [] | Pressed values in multiple mode; an empty array presses the allValue chip. | No |
onCategoriesChange | (categories: string[]) => void | none | Called with the new pressed values in multiple mode. | No |
allValue | string | all | Value of the chip that clears the selection in multiple mode. Defaults to "all". | No |
className | string | none | Custom className. | No |
size | "sm" | "md" | "lg" | md | Size variant. | No |
variant | "pills" | "underline" | "minimal" | pills | Style variant. | No |
collapsible | boolean | false | Collapse to the active chip and a disclosure button; expanding shows every chip and picking one collapses back to it. | No |
disclosureLabel | string | none | Accessible name of the disclosure button; defaults to the translated "All categories". | No |
groupLabel | string | none | Accessible name of the filter group; defaults to the translated "Filter by category" (categoryFilterGroupLabel). | No |
Variants
- Available variants:
pills,underline,minimal(defaultpills). - Available sizes:
sm,md,lg(defaultmd).
Invalid combinations
The contract records no invalid combinations.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves between filter buttons. |
| Enter / Space | Activates the focused filter. |
| Enter / Space | On the disclosure button (collapsible only): expands or collapses the chip row; picking a chip collapses it again. |
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST | ARIA: Renders a role=group named by groupLabel, or by the translated 'Filter by category' (categoryFilterGroupLabel) when it is not passed. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: The active filter sets aria-pressed='true' on a toggle button (not aria-current) because the filter is a UI control, not a navigation target; exactly one is pressed at a time (single-select). | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: With collapsible, a leading disclosure button named by disclosureLabel (default: the translated 'All categories') carries aria-expanded. It has no aria-controls, because it sits inside the group it would point at. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { CategoryFilter } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/patternsexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-category-filter>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.