Active Filters
A readout of the filters currently narrowing a list, as removable chips with a clear-all shortcut.
Preview
<x-ui.active-filters
:filters="[
['key' => 'status', 'label' => __('Status'), 'value' => __('Awaiting payment'), 'href' => '?status='],
['key' => 'warehouse', 'label' => __('Warehouse'), 'value' => __('Rotterdam'), 'href' => '?warehouse='],
['key' => 'carrier', 'label' => __('Carrier'), 'value' => 'PostNL', 'href' => '?carrier='],
]"
clear-href="?"
/>
Installation
php artisan ui:add active-filters
Note
This component ships an Alpine behavior module at
resources/js/ui/active-filters.js. Import it once from your bundle so it registers on alpine:init:
import './active-filters.js';
Registry contract
php artisan ui:add active-filters
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/active-filters.blade.php
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Active Filters (`active-filters`)
A readout of the filters currently narrowing a list, as removable chips with a clear-all shortcut.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add active-filters
```
## Usage
```blade
<x-ui.active-filters
:filters="[
['key' => 'status', 'label' => __('Status'), 'value' => __('Awaiting payment'), 'href' => '?status='],
['key' => 'warehouse', 'label' => __('Warehouse'), 'value' => __('Rotterdam'), 'href' => '?warehouse='],
['key' => 'carrier', 'label' => __('Carrier'), 'value' => 'PostNL', 'href' => '?carrier='],
]"
clear-href="?"
/>
```
## Props
- `filters` (array, default `[]`) — Active filters to render as chips: each entry is ['key','label','value','href'], where href removes only that filter. Pass 'event' (+ optional 'payload') instead of 'href' to dispatch rather than navigate; an entry with neither renders an inert, unremovable chip. Optional per entry: 'operator' (is, is not, before…) shown between the field and the value, 'values' (a list) instead of 'value', and 'edit' (+ 'editPayload'), an event the chip body dispatches to reopen the picker while a separate × removes.
- `clearHref` (string|null, default `null`) — Href that removes every filter at once. Shown only when more than one filter is active.
- `clearEvent` (string|null, default `null`) — Event (+ optional clearPayload) that removes every filter at once, dispatched instead of navigating. Same href/event rule as a chip.
- `clearPayload` (mixed|null, default `null`) — Payload dispatched with clearEvent. Defaults to an empty object.
- `label` (string|null, default `null`) — Leading caption before the chips. Defaults to a localized "Filtered by".
- `clearLabel` (string|null, default `null`) — Label for the clear-all control. Defaults to a localized "Clear all".
- `tone` (neutral|info, default `neutral`) — Chip colour: neutral uses the quiet secondary fill with foreground text, so the readout does not compete with the primary action or a status colour; info keeps the soft blue chips of 1.2.
## Use when
- Use for a small set of mutually exclusive options that users should compare visibly before choosing.
- Showing which filters currently narrow a list, table, or search result set.
- Letting an operator remove one active filter, or all of them, without reopening the picker.
## Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- Picking or editing filter values — use <x-ui.filters>, the facet picker, and reserve this for the readout of what it produced.
- There are no active filters; render nothing rather than an empty toolbar row.
## Anti-patterns
- Hiding a small comparable set in a dropdown
## Rules
- Use the `<brok:active-filters>` tag (or `<x-ui.active-filters>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/active-filters
- Registry JSON (files, props, contract): https://brokui.dev/r/open/active-filters.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- Each chip decides for itself: a link chip navigates, a dispatching chip
removes a filter that only lives in Alpine, and the unremovable chip is
an inert span with no close icon — all three side by side. --}}
<div
x-data="{ last: null }"
x-on:orders:set-filter="last = JSON.stringify($event.detail)"
class="flex flex-col gap-3"
>
<x-ui.active-filters
:filters="[
['key' => 'warehouse', 'label' => __('Warehouse'), 'value' => __('Rotterdam'), 'href' => '?warehouse='],
[
'key' => 'status',
'label' => __('Status'),
'value' => __('Awaiting payment'),
'event' => 'orders:set-filter',
'payload' => ['key' => 'status', 'value' => null],
],
['key' => 'carrier', 'label' => __('Carrier'), 'value' => 'PostNL'],
]"
clear-event="orders:set-filter"
:clear-payload="['key' => 'all', 'value' => null]"
/>
<p class="text-xs text-muted-foreground">
{{ __('Host received:') }}
<span data-slot="preview-event-log" class="font-mono text-foreground" x-text="last ?? '—'"></span>
</p>
</div>
Long Content
<div class="max-w-xs">
<x-ui.active-filters
:filters="[
[
'key' => 'customer',
'label' => __('Customer'),
'value' => __('A deliberately long customer name that verifies truncation instead of layout overflow'),
'href' => '?customer=',
],
['key' => 'status', 'label' => __('Status'), 'value' => __('Backordered'), 'href' => '?status='],
[
'key' => 'project',
'label' => __('Project'),
'operator' => __('is'),
'value' => __('Quarterly infrastructure migration for the northern region warehouses'),
'event' => 'preview:remove-filter',
'edit' => 'preview:edit-filter',
],
]"
clear-href="?"
/>
</div>
Operators
{{-- Chips that state the whole condition: field, operator and value(s).
The body of each chip dispatches `issues:edit-filter` to reopen its
picker; the separate × dispatches `issues:remove-filter`. --}}
<div
x-data="{ last: null }"
x-on:issues:edit-filter="last = 'edit ' + $event.detail.key"
x-on:issues:remove-filter="last = 'remove ' + $event.detail.key"
class="flex flex-col gap-3"
>
<x-ui.active-filters
:filters="[
[
'key' => 'status',
'label' => __('Status'),
'operator' => __('is not'),
'value' => __('Done'),
'event' => 'issues:remove-filter',
'payload' => ['key' => 'status'],
'edit' => 'issues:edit-filter',
'editPayload' => ['key' => 'status'],
],
[
'key' => 'labels',
'label' => __('Labels'),
'operator' => __('includes'),
'values' => [__('Bug'), __('Design'), __('Billing')],
'event' => 'issues:remove-filter',
'payload' => ['key' => 'labels'],
'edit' => 'issues:edit-filter',
'editPayload' => ['key' => 'labels'],
],
[
'key' => 'due',
'label' => __('Due date'),
'operator' => __('before'),
'value' => '2026-10-31',
'event' => 'issues:remove-filter',
'payload' => ['key' => 'due'],
],
]"
clear-event="issues:remove-filter"
:clear-payload="['key' => 'all']"
/>
<p class="text-xs text-muted-foreground">
{{ __('Host received:') }}
<span data-slot="preview-event-log" class="font-mono text-foreground" x-text="last ?? '—'"></span>
</p>
</div>
<div class="flex flex-col gap-4">
<x-ui.active-filters
:filters="[
['key' => 'status', 'label' => __('Status'), 'value' => __('In progress'), 'href' => '?status='],
['key' => 'assignee', 'label' => __('Assignee'), 'value' => 'Priya Raman', 'href' => '?assignee='],
]"
clear-href="?"
/>
<x-ui.active-filters
tone="info"
:label="__('Filtered by (info)')"
:filters="[
['key' => 'status', 'label' => __('Status'), 'value' => __('In progress'), 'href' => '?status='],
['key' => 'assignee', 'label' => __('Assignee'), 'value' => 'Priya Raman', 'href' => '?assignee='],
]"
clear-href="?"
/>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| filters | array | [] | Active filters to render as chips: each entry is ['key','label','value','href'], where href removes only that filter. Pass 'event' (+ optional 'payload') instead of 'href' to dispatch rather than navigate; an entry with neither renders an inert, unremovable chip. Optional per entry: 'operator' (is, is not, before…) shown between the field and the value, 'values' (a list) instead of 'value', and 'edit' (+ 'editPayload'), an event the chip body dispatches to reopen the picker while a separate × removes. |
| clearHref | string | null | null | Href that removes every filter at once. Shown only when more than one filter is active. |
| clearEvent | string | null | null | Event (+ optional clearPayload) that removes every filter at once, dispatched instead of navigating. Same href/event rule as a chip. |
| clearPayload | mixed | null | null | Payload dispatched with clearEvent. Defaults to an empty object. |
| label | string | null | null | Leading caption before the chips. Defaults to a localized "Filtered by". |
| clearLabel | string | null | null | Label for the clear-all control. Defaults to a localized "Clear all". |
| tone | neutral | info | neutral | Chip colour: neutral uses the quiet secondary fill with foreground text, so the readout does not compete with the primary action or a status colour; info keeps the soft blue chips of 1.2. |
Slots
Default Blade slot only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders nothing when the filters array is empty, instead of an empty toolbar row.
- Each chip decides for itself: href renders a plain link (unchanged since 1.0.0); event renders a button that dispatches that event (payload defaults to the whole entry) for a list that holds its own state; neither renders an inert, unremovable chip with no close icon.
- clearHref/clearEvent follow the same rule for the single clear-all control, which only appears once more than one filter is active.
- The component grows its own x-data root only when at least one entry actually needs to dispatch, so an all-href list renders exactly the markup 1.0.0 produced.
- A chip with values shows up to two of them ("Todo, Done"); more collapse to a localized ":count values" summary, with the full list in the title and the accessible name.
- An entry with edit splits the chip: the body is a button that dispatches edit (payload editPayload, else the whole entry) to reopen the picker, and a separate × control removes the filter through href or event. Without edit the chip is unchanged: the whole chip removes.
- Chips are neutral (the secondary badge fill) by default; tone="info" restores the soft-info blue chips.
- A chip stays on one line: a long value truncates (the field label keeps at most 45% of the chip), the × keeps its place, and the full condition is in the chip's title and accessible names. The split chip's 16px × has a 24x24 pointer target.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, authoredStateFixtures, responsive, rtl, darkMode, localized.
Guidance
Choose one option from a small visible set.
Use when
- Use for a small set of mutually exclusive options that users should compare visibly before choosing.
- Showing which filters currently narrow a list, table, or search result set.
- Letting an operator remove one active filter, or all of them, without reopening the picker.
Avoid when
- Do not hide a small option set in a dropdown when recognition and comparison matter.
- Picking or editing filter values — use <x-ui.filters>, the facet picker, and reserve this for the readout of what it produced.
- There are no active filters; render nothing rather than an empty toolbar row.
Use instead
- Select or combobox for long option sets
Anti-patterns
- Hiding a small comparable set in a dropdown
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- native
- Focus
native
- Meet the WCAG 2.2 AA target declared in meta.a11y.
- Each removable control is named for the whole condition it removes ("Remove filter: Status is not Todo"), including the operator and every value, not just the bare value.
- A split chip exposes two controls, each with its own focus ring: "Edit filter: …" for the body and "Remove filter: …" for the ×.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="active-filters-{{ $record->id }}">
<x-ui.active-filters
:filters="[
['key' => 'status', 'label' => __('Status'), 'value' => __('Awaiting payment'), 'href' => '?status='],
['key' => 'warehouse', 'label' => __('Warehouse'), 'value' => __('Rotterdam'), 'href' => '?warehouse='],
['key' => 'carrier', 'label' => __('Carrier'), 'value' => 'PostNL', 'href' => '?carrier='],
]"
clear-href="?"
/>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Active Filters.
The readout of what is currently narrowing a list — never the picker
itself (that's <x-ui.filters>). Each chip names the field and the value
applied to it and removes only that one filter; "clear all" only shows up
once there is more than one active filter to be worth a shortcut for.
Each chip decides for itself how it removes a filter — this is not a
component-wide mode, because a single list can legitimately mix both:
href set (default, unchanged since 1.0.0) — a plain `<a>`, so
removing a filter survives a middle-click, a shared
address, and a page with no JavaScript.
event set — a `<button>` that dispatches that event (with an optional
`payload`, defaulting to the whole filter entry) for a list
that holds its own state (an Alpine store, a Livewire
component, a preview with no server to navigate to).
neither — an inert `<span>`, no close icon. Before this, an entry
with no `href` still rendered as a link to `#` — a dead
control that looked removable and wasn't. A chip the
caller does not want removable is now inert instead of
broken.
`clearHref`/`clearEvent` follow the same rule for the single "clear all"
control. The component grows its own `x-data` root only when at least one
entry actually needs to dispatch, so the plain-links case stays exactly
the markup 1.0.0 produced.
A chip can also state the whole condition: an optional `operator`
("is not", "before") between the field and the value, and `values` (a
list) instead of `value`. Two values read "Todo, Done"; more read
":count values", with every value in the chip's title and accessible name.
An entry with `edit` (an event name, + optional `editPayload`) splits the
chip in two: the body is a button that dispatches `edit` (reopen the
picker), and a separate × removes the filter through `href` or `event`.
`tone` sets the chip colour: neutral (default since 1.3.0) is the quiet
secondary fill with foreground text, so the readout does not compete with
the page's primary action or a status colour; info keeps the soft blue
chips of 1.x.
Generalised from `*/applied-filters.blade.php` across the source admin:
the domain-specific i18n keys and the page's own Alpine store
(`appliedFilters`/`setFilter`/`clearFilters`) are left out on purpose —
callers pass plain `href`s (or, now, `event`s) built from their own state.
--}}
@props([
// [['key' => 'status', 'label' => 'Status', 'value' => 'Open',
// 'href' => '?status='], ...] — or, per entry, 'event' => 'name'
// (+ optional 'payload') instead of 'href' to dispatch rather than
// navigate. An entry with neither renders an inert, unremovable chip.
// Optional per entry: 'operator' => 'is not', 'values' => ['Todo', …],
// 'edit' => 'event-name' (+ 'editPayload') to reopen the picker.
'filters' => [],
// Href that removes every filter at once. Hidden unless more than one
// filter is active — a single chip is already its own "clear" affordance.
'clearHref' => null,
// Event (+ optional clearPayload) that removes every filter at once,
// for a list that owns its own state. Same href/event rule as a chip.
'clearEvent' => null,
'clearPayload' => null,
'label' => null,
'clearLabel' => null,
// Chip colour: neutral (default, the secondary fill) | info (soft blue).
'tone' => 'neutral',
])
@php
$filters = array_values(array_filter((array) $filters, 'is_array'));
$label ??= __('Filtered by');
$clearLabel ??= __('Clear all');
// Tone → badge variant. Unknown values fall back to neutral.
$chipVariant = ['neutral' => 'secondary', 'info' => 'soft-info'][$tone] ?? 'secondary';
$tone = $chipVariant === 'soft-info' ? 'info' : 'neutral';
$showClear = count($filters) > 1 && ((bool) $clearHref || (bool) $clearEvent);
$dispatchExpr = static function (string $event, $payload): string {
return '$dispatch('.\Illuminate\Support\Js::from($event).', '.\Illuminate\Support\Js::from($payload).')';
};
// Precomputed per chip, because Blade's component-tag parser reads a tag
// with a regex: a directive written between its attributes is not seen,
// so each mode has to arrive as a bound attribute that is null when
// unused — a null-valued bound attribute is dropped from the tag rather
// than rendered empty.
// The value text a chip shows: `values` (a list) wins over `value`. Two
// values are named; more collapse to a count, so a chip stays one line.
$valueList = static fn (array $filter): array => array_values(array_filter(
array_map('strval', (array) ($filter['values'] ?? [])),
static fn (string $value): bool => $value !== '',
));
$valueText = static function (array $filter) use ($valueList): string {
$values = $valueList($filter);
if ($values === []) {
return (string) ($filter['value'] ?? '');
}
return count($values) <= 2
? implode(', ', $values)
: __(':count values', ['count' => count($values)]);
};
// The whole condition in words, for accessible names and the title.
$condition = static function (array $filter) use ($valueList): string {
$values = $valueList($filter);
$value = $values === [] ? (string) ($filter['value'] ?? '') : implode(', ', $values);
return trim(implode(' ', array_filter([
(string) ($filter['label'] ?? ''),
(string) ($filter['operator'] ?? ''),
$value,
], static fn (string $part): bool => $part !== '')));
};
$chipEditHandler = static fn (array $filter): ?string => ! empty($filter['edit'])
? $dispatchExpr((string) $filter['edit'], $filter['editPayload'] ?? $filter)
: null;
$chipTag = static fn (array $filter): string => match (true) {
! empty($filter['href']) => 'a',
! empty($filter['event']) => 'button',
default => 'span',
};
$chipHref = static fn (array $filter): ?string => $filter['href'] ?? null;
$chipHandler = static fn (array $filter): ?string => (empty($filter['href']) && ! empty($filter['event']))
? $dispatchExpr((string) $filter['event'], $filter['payload'] ?? $filter)
: null;
$chipRemovable = static fn (array $filter): bool => ! empty($filter['href']) || ! empty($filter['event']);
$chipAriaLabel = static fn (array $filter): ?string => $chipRemovable($filter)
? __('Remove filter: :condition', ['condition' => $condition($filter)])
: null;
$needsAlpine = $clearEvent || collect($filters)->contains(
static fn (array $filter): bool => (empty($filter['href']) && ! empty($filter['event'])) || ! empty($filter['edit'])
);
$partFocus = 'rounded-sm outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring';
// The badge wraps its slot in one inline label span. A chip lays that
// span out as a no-wrap flex row, so a long label truncates while the
// remove glyph keeps its place on the same line instead of dropping
// below it (the split chip's edit button is a flex box, which broke the
// inline flow). The full condition stays in the title and the names.
$chipRow = '[&>[data-slot=badge-label]]:flex [&>[data-slot=badge-label]]:items-center [&>[data-slot=badge-label]]:gap-1';
$clearHandler = $clearEvent ? $dispatchExpr((string) $clearEvent, $clearPayload ?? []) : null;
@endphp
@if (count($filters))
<div
data-slot="active-filters"
data-tone="{{ $tone }}"
@if ($needsAlpine) x-data="{}" @endif
{{ $attributes->merge(['class' => 'flex flex-wrap items-center gap-2']) }}
>
<span data-slot="active-filters-label" class="shrink-0 text-xs text-muted-foreground">
{{ $label }}
</span>
@foreach ($filters as $filter)
@if (! empty($filter['edit']))
{{-- Split chip: the body reopens the picker, the × removes. --}}
<x-ui.badge
:variant="$chipVariant"
size="status"
data-filter-chip="split"
class="min-w-0 max-w-[16rem] gap-1 ps-2 pe-1 !overflow-visible {{ $chipRow }} [&>[data-slot=badge-label]]:overflow-visible"
:title="$condition($filter)"
>
<button
type="button"
data-slot="active-filters-edit"
x-on:click="{{ $chipEditHandler($filter) }}"
aria-label="{{ __('Edit filter: :condition', ['condition' => $condition($filter)]) }}"
class="flex min-w-0 items-center gap-1 {{ $partFocus }}"
>
<span class="min-w-0 max-w-[45%] shrink-0 truncate opacity-70">{{ $filter['label'] ?? '' }}</span>
@if (filled($filter['operator'] ?? null))
<span data-slot="active-filters-operator" class="shrink-0 opacity-70">{{ $filter['operator'] }}</span>
@endif
<span class="min-w-0 truncate font-medium">{{ $valueText($filter) }}</span>
</button>
@if ($chipRemovable($filter))
<{{ $chipTag($filter) }}
data-slot="active-filters-remove"
@if ($chipTag($filter) === 'a')
href="{{ $chipHref($filter) }}"
@else
type="button"
x-on:click="{{ $chipHandler($filter) }}"
@endif
aria-label="{{ $chipAriaLabel($filter) }}"
title="{{ $chipAriaLabel($filter) }}"
class="relative inline-flex size-4 shrink-0 items-center justify-center before:absolute before:-inset-1 hover:bg-current/10 {{ $partFocus }}"
>
<svg aria-hidden="true" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-2">
<path d="M3 3 11 11M11 3 3 11" />
</svg>
</{{ $chipTag($filter) }}>
@endif
</x-ui.badge>
@else
<x-ui.badge
:as="$chipTag($filter)"
:href="$chipHref($filter)"
:x-on:click="$chipHandler($filter)"
:variant="$chipVariant"
size="status"
class="min-w-0 max-w-[16rem] gap-2 ps-2 pe-2 {{ $chipRow }}"
:aria-label="$chipAriaLabel($filter)"
:title="$condition($filter)"
>
<span class="min-w-0 max-w-[45%] shrink-0 truncate opacity-70">{{ $filter['label'] ?? '' }}</span>
@if (filled($filter['operator'] ?? null))
<span data-slot="active-filters-operator" class="shrink-0 opacity-70">{{ $filter['operator'] }}</span>
@endif
<span class="min-w-0 truncate font-medium">{{ $valueText($filter) }}</span>
@if ($chipRemovable($filter))
<svg aria-hidden="true" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-2 shrink-0">
<path d="M3 3 11 11M11 3 3 11" />
</svg>
@endif
</x-ui.badge>
@endif
@endforeach
@if ($showClear)
<{{ $clearEvent ? 'button' : 'a' }}
data-slot="active-filters-clear"
@if ($clearEvent)
type="button"
x-on:click="{{ $clearHandler }}"
@else
href="{{ $clearHref }}"
@endif
class="shrink-0 text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
>
{{ $clearLabel }}
</{{ $clearEvent ? 'button' : 'a' }}>
@endif
</div>
@endif
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.2 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0