Canvas Design System
Main Site Tokens

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.
Don't use when: You need search alone with no filters — use Input with type: 'search'. For client-side switching between a few fixed sets, Tabs is lighter.
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.

expand_more
expand_more
<?= 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.

expand_more
Active filters:
Design Sprints
Clear all
<?= ws_filter_bar($filters, [
    'activeChips' => [['label' => 'Design Sprints', 'name' => 'category_id', 'value' => 1]],
]) ?>

States

StateBehavior
DefaultA GET form. Submitting puts search and filter values in the query string, so filtered views are linkable and survive a reload.
Filter selectedPass the filter's current value so the dropdown reflects the query string on redisplay.
FocusSearch and selects use the shared --focus-ring-width / --focus-ring-offset geometry.
Chips shownRendered 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 removalHandled by wsFilterBarRemove() in components.js: drops that one parameter, resets page, keeps the rest of the query string and the hash, then reloads.
Empty resultsNot 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

OptionTypeDefaultPurpose
$filtersarray[]Positional. Dropdown configs; may be empty for search-only.
searcharraynull['placeholder' => …, 'value' => …, 'name' => 'q']. Omit for filters only.
activeChipsarray[][['label' => …, 'name' => …, 'value' => …]]. You build this from the query string.
colorstring'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.
classstring''Additional CSS classes on the container
attrsarray[]Extra HTML attributes on the container

Filter options

OptionTypeDefaultPurpose
namestring''Query-string parameter name
labelstring''Dropdown label
optionsarray[]Flat value => label pairs, as ws_select() takes
valuestring''Currently selected value

Accessibility

ConcernBehavior
SemanticsA real GET <form> with native inputs, so filtered views are linkable and the back button behaves.
KeyboardNative throughout: Tab across search and selects, Enter in the search field submits.
Chip remove buttonsLabelled 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 resultsBecause 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 reloadA 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 iconDecorative. The field's own label and placeholder carry the meaning.

Tokens

TokenUsed for
--bg-surfaceBar and search-field background
--color-borderBar and field borders
--color-ink / --color-ink-mutedField 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-offsetFocus geometry on search and selects
--radius-lgBar and field corners
--space-2 / --space-3 / --space-4 / --space-6Control spacing and the chips row
--text-caption / --text-uiChip and control type scale
--font-body / --font-mediumFace and weight
--transition-fastHover and focus transitions

CSS Classes

ClassPurpose
.ws-filter-barThe form container
.ws-filter-bar--plan / --facilitate / --reflectColour keys. --learn and --neutral are emitted by the template but have no CSS rule.
.ws-filter-bar__controlsSearch and selects row
.ws-filter-bar__search / __search-input / __search-iconSearch field and its icon
.ws-filter-bar__selectsDropdown group
.ws-filter-bar__submitSubmit button
.ws-filter-bar__chips / __chips-labelActive-chip row and its label
.ws-filter-bar__clear-all"Clear all" link
.ws-filter-chipAn individual chip. Carries data-filter-name and data-filter-value.
.ws-filter-chip__clearChip remove button (the one calling the undefined function)

Files

FilePurpose
includes/components/helpers.phpws_filter_bar() helper function
includes/components/filter-bar.phpTemplate — form, search, chips. Renders ws_select() for each filter.
includes/components/components.cssStyles (.ws-filter-bar and .ws-filter-chip rules)
includes/components/components.jswsFilterBarRemove() — chip removal