Filters
Filter bar + applied-filter chips. Each chip's remove button names its filter in
the aria-label. Filtering itself is your code (form submit or HTMX).
Not for one or two fields — a single search input in the
table toolbar reads better
than a bar and chips; the chips earn their place once a user can have
several filters applied at once and lose track of them.
Filtering something — bar + multi-select + matching rows
| PO # | Cost center | Amount |
|---|---|---|
| PO-88210 | CC-4021 | $4,208.00 |
| PO-88211 | CC-4021 | $18,940.50 |
The full loop: the
multi-select dropdown is where chip values
come from, the active chip echoes the applied filter, the rows below match it.
Also the base vs --active chip contrast — an inactive chip
("Overdue only") is an available filter, an active one is applied and
removable.
Saved views opt-in
Views are just links (server-rendered, zero new CSS) — but which one is
"active" and what the filter bar's fields show shouldn't be hand-set per
page. initSavedViews() derives both from the URL: it reads
location.search, fills matching .bo-filter-bar
fields, and marks whichever [data-saved-views] link's own
querystring matches the current one aria-current="page" —
clearing it from the others. Try it: click a saved view below, watch the
filter bar and the active chip update to match.
Markup
<!-- A real <form>, so type="reset" works and role="search" sits on it. -->
<form class="bo-filter-bar" role="search" aria-label="Invoice filters">
<input class="bo-input" type="search" name="q" aria-label="Search invoices" />
<select class="bo-select" name="status" aria-label="Status">…</select>
<span class="bo-chip bo-chip--active">Status: Pending
<button class="bo-chip__remove" type="button"
aria-label="Remove filter Status: Pending">✕</button>
</span>
<button class="bo-btn bo-btn--ghost" type="reset">Clear all</button>
</form> Quick reference generated
| Class | Role |
|---|---|
| .bo-chip | block (root) |
| .bo-chip--active | variant: active |
| .bo-chip__remove | part: remove |
| .bo-filter-bar | block (root) |
| .bo-form-field | block (root) |
State attributes: data-saved-views
API generated from CSS
| Blocks | bo-chipbo-filter-barbo-form-field |
|---|---|
| Parts | bo-chip__remove |
| Variants | bo-chip--active |
| data-* hooks | data-saved-views |
| JS required | Optional: initSavedViews() — populates a .bo-filter-bar's fields from the URL querystring and marks the matching [data-saved-views] link aria-current. |
- Make the bar a real
<form role="search">sotype="reset"works. - Chip remove buttons name their filter in
aria-label; 24px hit area is built in. - Saved views are just links — persistence (naming, saving, deleting a view) is your code; the behavior only keeps the UI in sync with whichever view the URL says is active.
Contrast — 27 AA pairs this component uses (generated, CI-gated)
| Text | on | Light | Dark |
|---|---|---|---|
| text-primary | bg-surface | 17.74 | 16.15 |
| text-primary | bg-canvas | 16.98 | 18.08 |
| text-secondary | bg-surface | 10.31 | 11.46 |
| text-secondary | bg-muted | 9.37 | 9.75 |
| text-muted | bg-surface | 7.56 | 6.65 |
| text-muted | bg-muted | 6.87 | 5.66 |
| bg-surface | text-primary | 17.74 | 16.15 |
| accent-text | accent-subtle | 7.27 | 8.38 |
| danger-text | bg-surface | 6.47 | 8.89 |
| success-text | bg-surface | 5.02 | 12.02 |
| accent | bg-surface | 5.47 | 9.07 |
| text-primary | bg-hover | 16.12 | 13.75 |
| text-primary | bg-selected | 17.01 | 11.86 |
| accent-text | bg-selected | 7.27 | 8.38 |
| text-primary | bg-surface-raised | 17.74 | 14.51 |
| border-control | bg-surface | 4.83 | 6.65 |
| accent | bg-selected | 5.25 | 6.66 |
| --bo-state-error-color | bg-surface | 4.83 | 6.1 |
| accent | accent-subtle | 5.25 | 6.66 |
| border-control | bg-muted | 4.39 | 5.66 |
| accent | bg-muted | 4.97 | 7.72 |
| warning-strong | bg-muted | 4.56 | 6.69 |
| warning-strong | bg-surface | 5.02 | 7.86 |
| danger | bg-muted | 4.39 | 5.19 |
| text-primary | bg-muted | 16.12 | 13.75 |
| success-text | bg-muted | 4.56 | 10.23 |
| danger-text | bg-muted | 5.88 | 7.57 |
Maturity generated
- Introduced
- v0.1.0
- CSS floor
- Chrome/Edge 99 · Firefox 97 · Safari 15.4
- Alignment scored
- 2026-08-21
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.1.0.
The floor is derived from this component's stylesheet alone — set by
@layer — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (18/18) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-21.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | no raw font-size |
| Colour | 3 / 3 | zero raw hex |
| Spacing | 3 / 3 | 0.125rem chip hug commented (94.2); the 1rem remove box already carried its WCAG 2.5.8 hit-area note |
| Interaction | 3 / 3 | composes initDropdowns + initSavedViews; applied filters are visible chips, not state hidden behind a dialog |
| Content | 3 / 3 | the opener says a bar and chips are wrong for one or two fields, pointing at a search input in the table toolbar, and says what earns the chips (94.8) |
| Fit | 3 / 3 | filter bar above tables/lists — the context list-report actually uses |