Filter Bar
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 — exercise lists, workshop catalogues, saved-plan tables. It standardises controls that were previously rebuilt per page.
check_circle
Fixed 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.
<?= ws_filter_bar([
['name' => 'category_id', 'label' => 'Category',
'options' => [1 => 'Design Sprints', 2 => 'Icebreakers'], 'value' => 1],
['name' => 'duration', 'label' => 'Duration',
'options' => ['short' => 'Under 30 min', 'long' => 'Over 1 hour']],
], [
'search' => ['placeholder' => 'Search activities…'],
]) ?>Search only
Pass an empty filters array. The bar still gives you the submit wiring and consistent spacing.
<?= ws_filter_bar([], ['search' => ['placeholder' => 'Search workshops…', 'name' => 'q']]) ?>
With active chips
Chips summarise what's applied, with a "Clear all" alongside. Shown here for completeness — the remove buttons don't work as shipped.
<?= ws_filter_bar($filters, [
'activeChips' => [['label' => 'Design Sprints', 'name' => 'category_id', 'value' => 1]],
]) ?>States
| State | Behavior |
|---|---|
| Default | 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. 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.
<?php
$chips = [];
if (!empty($_GET['category_id'])) {
$chips[] = ['label' => $categoryNames[$_GET['category_id']],
'name' => 'category_id',
'value' => $_GET['category_id']];
}
if (!empty($_GET['duration'])) {
$chips[] = ['label' => $durationLabels[$_GET['duration']],
'name' => 'duration',
'value' => $_GET['duration']];
}
?>
<?= ws_filter_bar($filters, [
'search' => ['placeholder' => 'Search activities…', 'value' => $_GET['q'] ?? ''],
'activeChips' => $chips,
]) ?>
<!-- Chip removal needs no wiring — wsFilterBarRemove() ships in components.js -->
<?php if (!$results): ?>
<?= ws_empty_state('Nothing matches those filters', [
'icon' => 'filter_alt_off', 'action' => 'Clear filters', 'actionHref' => '?',
]) ?>
<?php endif; ?>Options
Bar options
| Option | Type | Default | Purpose |
|---|---|---|---|
$filters | array | [] | Positional. Dropdown configs; may be empty for search-only. |
search | array | null | ['placeholder' => …, 'value' => …, 'name' => 'q']. Omit for filters only. |
activeChips | array | [] | [['label' => …, 'name' => …, 'value' => …]]. You build this from the query string. |
color | string | 'learn' | learn | plan | facilitate | reflect | neutral. Note that learn and neutral have no CSS rule, so they render with base styling — the default is effectively unstyled. |
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 | '' | Dropdown label |
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. |
| Keyboard | Native throughout: Tab across search and selects, Enter in the search field submits. |
| Chip remove buttons | Labelled aria-label="Remove filter". The label is generic — in a row of chips every one announces identically. Naming the filter would be better: "Remove Design Sprints filter". |
| 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. The field's own label and placeholder carry the meaning. |
Tokens
| Token | Used for |
|---|---|
--bg-surface | Bar and search-field background |
--color-border | Bar and field borders |
--color-ink / --color-ink-muted | Field text and the chips label |
--phase-plan / --phase-facilitate / --phase-reflect (and -light pairs) | Colour-key accents (all red since Red Unification) |
--focus-ring-width / --focus-ring-offset | Focus geometry on search and selects |
--radius-lg | Bar and field corners |
--space-2 / --space-3 / --space-4 / --space-6 | Control spacing and the chips row |
--text-caption / --text-ui | Chip and control type scale |
--font-body / --font-medium | Face and weight |
--transition-fast | Hover and focus transitions |
CSS Classes
| Class | Purpose |
|---|---|
.ws-filter-bar | The form container |
.ws-filter-bar--plan / --facilitate / --reflect | Colour keys. --learn and --neutral are emitted by the template but have no CSS rule. |
.ws-filter-bar__controls | Search and selects row |
.ws-filter-bar__search / __search-input / __search-icon | Search field and its icon |
.ws-filter-bar__selects | Dropdown group |
.ws-filter-bar__submit | Submit button |
.ws-filter-bar__chips / __chips-label | Active-chip row and its label |
.ws-filter-bar__clear-all | "Clear all" link |
.ws-filter-chip | An individual chip. Carries data-filter-name and data-filter-value. |
.ws-filter-chip__clear | Chip remove button (the one calling the undefined function) |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_filter_bar() helper function |
includes/components/filter-bar.php | Template — form, search, chips. Renders ws_select() for each filter. |
includes/components/components.css | Styles (.ws-filter-bar and .ws-filter-chip rules) |
includes/components/components.js | wsFilterBarRemove() — chip removal |