The controls above a listing grid: a search box, dropdown filters, and chips for what's currently applied.
When to Use
Use when:
A listing needs GET-based search or filtering, with or without filters — exercise lists, workshop catalogues, saved-plan tables, a search box above "My workshops". It standardises controls that were previously rebuilt per page. Its "Clear all" link beside the chips is the one clear control DESIGN.md 11.6 allows: it loads the unfiltered list, and Back undoes it.
Don't use when:
The search field belongs to a form about something else, or sits in the site header rather than above a list — use Input with type: 'search'. For client-side switching between a few fixed sets, Tabs is lighter.
check_circleFixed July 2026. Chip remove buttons call wsFilterBarRemove(name), which the template referenced but nothing defined — every chip threw ReferenceError: wsFilterBarRemove is not defined. It now ships in components.js: it drops that parameter from the query string, resets page, and reloads, preserving all other filters and the hash. No consumer code required.
Variants
Search and filters
The common case. Filters render through Select internally, so they inherit its styling and focus treatment.
The search box above a list, with no filters: pass an empty filters array. The submit button reads "Search" rather than "Filter", and showSearchLabel puts the field's name above it as a visible label, which a placeholder is not.
A GET form. Submitting puts search and filter values in the query string, so filtered views are linkable and survive a reload.
Filter selected
Pass the filter's current value so the dropdown reflects the query string on redisplay.
Focus
Search and selects use the shared --focus-ring-width / --focus-ring-offset geometry.
Chips shown
Rendered whenever activeChips is non-empty, with a "Clear all" link: the one clear control DESIGN.md 11.6 allows, because it loads the unfiltered list and Back undoes it. Building the chip array is yours — the component doesn't derive it from the filters.
Chip removal
Handled by wsFilterBarRemove() in components.js: drops that one parameter, resets page, keeps the rest of the query string and the hash, then reloads.
Empty results
Not handled here. Pair the bar with Empty State below the grid.
Real-World Usage
The exercises listing. Chips are derived from the query string, and the missing remove function is supplied locally — it drops one parameter and reloads, which is all it needs to do.
The search input's accessible name (its aria-label). Defaults to the placeholder less any trailing ellipsis, or "Search". Set it when the placeholder is an example rather than a description.
showSearchLabel
bool
false
Shows searchLabel as a visible <label> above the field; the row then lines up on the inputs.
submitLabel
string
'Search' or 'Filter'
The submit button's text: "Search" with no filters, "Filter" with them.
keep
array
[]
Query-string names the search and "Clear all" carry through, as hidden fields: inside /app/, ['view', 'step'], or a search sends the person back to Home. Inside a tab panel of a ws_tabs with a param, the panel's own tab is carried too, without listing it.
activeChips
array
[]
[['label' => …, 'name' => …, 'value' => …]]. You build this from the query string.
color
string
'learn'
learn | plan | facilitate | reflect. learn, the default, has no rule of its own: it is the base look, and since the Red Unification the other three render the same.
class
string
''
Additional CSS classes on the container
attrs
array
[]
Extra HTML attributes on the container
Filter options
Option
Type
Default
Purpose
name
string
''
Query-string parameter name
label
string
''
The select's accessible name (its aria-label) and the text of its empty "any" option
options
array
[]
Flat value => label pairs, as ws_select() takes
value
string
''
Currently selected value
Accessibility
Concern
Behavior
Semantics
A real GET <form> with native inputs, so filtered views are linkable and the back button behaves.
Names
Every control is named without changing the look. Each select's aria-label is its filter label; the search input's is searchLabel, or the placeholder. They are aria-label rather than a hidden <label for> because a select's id defaults to its field name, and two bars with the same filter on one page would point both labels at the first select. A placeholder alone is not a name: it disappears as soon as someone types.
Search landmark
With a search field the form is role="search", named like the field, so a screen reader can jump straight to it. Two bars on one page need different searchLabels, or the landmark list shows two entries with the same name.
Keyboard
Native throughout: Tab across search and selects, Enter in the search field submits.
Chip remove buttons
Each names its filter, aria-label="Remove Design Sprints filter", so a row of chips does not announce the same words for every button.
Announcing results
Because submission reloads the page, the new result count is announced as part of the new page. If you ever move to async filtering, add a live region for the count.
Focus after reload
A full reload returns focus to the top of the document, so a keyboard user must tab back to the controls. Consider focusing the results heading on load when filters are applied.
Search icon
Decorative and aria-hidden, so the icon's ligature text is not read. The field's own name carries the meaning.