Skip to main content
WireKit
Copy for LLM

Filter Builder

A compact filtering UI for any list or table: active filters render as removable chips, and an Add filter popover walks the user through picking a field, an operator valid for that field's type, and a typed value. On every change it emits a normalized [{field, op, value}] array — ready to drive a server query or a client-side filter.

It composes form inputs behind a small Alpine state machine; there is no new styling vocabulary to learn. Each field declares its type, and the popover offers only the operators and value editor that make sense for it (no contains on a number, no free-text on a boolean).

Basic Usage

Pass an array of field definitions. Each is {key, label, type} with optional options (for select) and custom operators.

An empty filter bar — click Add filter

With Active Filters

Seed the bar with existing filters via :value. Each filter is a chip — click it to edit, or use the × to remove.

Pre-populated active filters

Set searchable to add a free-text box alongside the chips — handy when the list also supports keyword search. The box dispatches a bubbling search-change event with the current term (independent of the structured filters), so you can bind it on its own: x-on:search-change="$wire.set('q', $event.detail.value)".

Filter bar with a search box

With A Result Count

The status slot sits below the chips inside an aria-live region — perfect for a result count that updates as filters change.

Filter bar with a result count
42 members match

Field Definitions

Each entry in fields describes one filterable column:

Key Required Description
key yes The field identifier emitted in the filter object
label yes The human-readable name shown in the chip + popover
type yes text · number · select · date · bool
options for select [{value, label}] choices for the value editor
operators no Override the default operator set: [{op, label}]

Operators By Type

The popover offers these operators automatically; override per field with operators:

Type Operators
text contains · is · starts with · ends with
number = · > · < · ≥ · ≤
select is · is not
date on · before · after
bool is

The Emitted Value

On every add / edit / remove / clear, filter-builder emits the normalized array two ways:

  1. A bubbling filter-change event with detail.filters.
  2. A JSON string written to a hidden input (so wire:model and plain forms both work).
{{-- 1. Bind the JSON bridge straight to a Livewire property --}}
<x-wirekit::filter-builder :fields="$fields" wire:model.live="filtersJson" />
// 2. Decode the JSON the bridge writes, then apply it to your query
public string $filtersJson = '[]';

public function rows()
{
    $filters = json_decode($this->filtersJson, true) ?: [];

    return collect($filters)->reduce(function ($query, $f) {
        // 3. Map (field, op, value) onto your query builder
        return match ($f['op']) {
            'contains' => $query->where($f['field'], 'like', "%{$f['value']}%"),
            'gt'       => $query->where($f['field'], '>', $f['value']),
            'is'       => $query->where($f['field'], $f['value']),
            default    => $query,
        };
    }, Order::query())->get();
}

Prefer events? Listen for filter-change and set the property yourself:

{{-- 4. Event-driven alternative — receive the array directly --}}
<x-wirekit::filter-builder
    :fields="$fields"
    x-on:filter-change="$wire.set('filters', $event.detail.filters)"
/>

Livewire Integration

<x-wirekit::filter-builder> keeps its active filters in internal state seeded from the value prop, so when binding with Livewire pass the bound property as :value alongside wire:model to seed the initial filter rows:

<x-wirekit::filter-builder wire:model.live="filters" :value="$filters" :fields="$fields" />

Without :value the builder starts with no filter rows until the user adds one; wire:model keeps it in sync afterward. This is the framework-agnostic seeding pattern WireKit's stateful controls share — it works in plain Blade forms too.

Props

Prop Type Default Description
fields array [] Field definitions (see above)
value array [] Initial active filters [{field, op, value}]
name string|null null Hidden-input name for plain-form submission
searchable bool false Render a free-text search box in the chip bar
searchPlaceholder string 'Search…' Placeholder for the search box
addLabel string 'Add filter' Label for the add-filter trigger
scope string|null null Scoped personalization name

Accessibility

  • Each chip is a button pair: the body opens the edit popover, the × removes the filter. Both are keyboard reachable and carry a descriptive aria-label ("Edit filter: Status is Open", "Remove filter: Status is Open").
  • The add-filter trigger exposes aria-haspopup="dialog" and aria-expanded.
  • The popover is a role="dialog" labeled by its heading. Opening it moves focus to the field selector; Escape, Cancel, and Apply return focus to the trigger. Clicking outside closes it without stealing focus back.
  • Every popover control (field / operator / value) is a labeled form element.
  • Wrap the status slot's result count in your own aria-live context (the slot is already inside a polite live region) so screen-reader users hear the count change.

Keyboard Interaction

Key Action
Tab Move between chips, the add-filter trigger, and clear-all
Enter / Space Open the popover (on the trigger) or edit (on a chip)
Enter Apply the draft filter (from the text value editor)
Escape Close the popover, returning focus to the trigger

Design Tokens

Element Token
Chip background --color-wk-bg-muted
Chip text --color-wk-text
Add-filter dashed border --color-wk-border
Popover surface --color-wk-bg-elevated
Popover shadow --shadow-wk-lg
Apply button --color-wk-accent
Remove-hover / clear-hover --color-wk-danger-text
Focus ring --color-wk-ring

Customization

Override the labels and the search default without publishing views via config/wirekit.php:

'components' => [
    'filter-builder' => [
        'searchable' => true,
        'search-placeholder' => 'Search records…',
        'add-label' => 'Add condition',
    ],
],

Further Reading