Filter
Collects a set of filter fields into one value, renders the active ones as removable chips, and publishes the result — the toolbar that usually sits above a data table.
It composes rather than replaces. Each filter is a
nana-filter-item wrapping whichever field suits
it — a search input, a status select, a date range, a column picker — so every
filter behaves the same from your side no matter which control drives it. The
aggregated value is a plain object keyed by each item’s name, ready to hand
to a query string or a fetch.
Preview
Section titled “Preview”<nana-filter label="Filters">
<nana-filter-item name="q" label="Search" grow live debounce="250">
<nana-input placeholder="Order ref or customer" clearable></nana-input>
</nana-filter-item>
<nana-filter-item name="status" label="Status" operator="is" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
<nana-option value="pending">Pending</nana-option>
<nana-option value="closed">Closed</nana-option>
</nana-select>
</nana-filter-item>
<nana-filter-item name="created" label="Created" operator="between">
<nana-date-range clearable></nana-date-range>
</nana-filter-item>
</nana-filter> Installation
Section titled “Installation”npm install @nana-tec/ui-componentsImport
Section titled “Import”import "@nana-tec/ui-components/filter";That one import registers nana-filter-item, nana-tag and nana-popup too,
since a filter surface is not usable without them. The fields you slot in are
imported separately.
Reading the value
Section titled “Reading the value”nana-filter-change carries the aggregated value — active filters only, keyed
by each item’s name.
const filter = document.querySelector("nana-filter");
filter.addEventListener("nana-filter-change", (e) => { const { value } = e.detail; // { q: "acme", status: "open" } loadOrders(new URLSearchParams(value));});The same object is readable at any time via the value property, and assigning
to it writes each key back through to its field:
// Restore from the URL on load — this does not emit an event back at you.filter.value = Object.fromEntries(new URLSearchParams(location.search));The four common filters
Section titled “The four common filters”Search
Section titled “Search”Give the search field grow so it absorbs the leftover width in a bar layout,
and live with a debounce so it filters as the user types without a request
per keystroke.
<nana-filter label="Filters">
<nana-filter-item name="q" label="Search" grow live debounce="250">
<nana-input placeholder="Search orders" clearable></nana-input>
</nana-filter-item>
</nana-filter> Status
Section titled “Status”Set default-value to whatever means “no filter”. Without it a select sitting
on All counts as an active filter and shows a chip forever.
<nana-filter label="Filters">
<nana-filter-item name="status" label="Status" operator="is" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
<nana-option value="pending">Pending</nana-option>
<nana-option value="closed">Closed</nana-option>
</nana-select>
</nana-filter-item>
</nana-filter> A segmented control reads faster when there are five or fewer statuses and they are the primary cut through the data:
<nana-filter label="Filters">
<nana-filter-item name="status" label="Status" default-value="all" no-label>
<nana-radio-group value="all" orientation="horizontal">
<nana-radio-button value="all">All</nana-radio-button>
<nana-radio-button value="open">Open</nana-radio-button>
<nana-radio-button value="pending">Pending</nana-radio-button>
<nana-radio-button value="closed">Closed</nana-radio-button>
</nana-radio-group>
</nana-filter-item>
</nana-filter> For several statuses at once, slot a checkbox group. The value becomes an array and the chip collapses the whole selection into one removable token.
<nana-filter label="Filters">
<nana-filter-item name="status" label="Status" operator="is any of">
<nana-checkbox-group orientation="horizontal">
<nana-checkbox value="open">Open</nana-checkbox>
<nana-checkbox value="pending">Pending</nana-checkbox>
<nana-checkbox value="closed">Closed</nana-checkbox>
</nana-checkbox-group>
</nana-filter-item>
</nana-filter> A date range serialises as start/end. The chip splits it back into two
readable ends rather than showing the raw value.
<nana-filter label="Filters">
<nana-filter-item name="created" label="Created" operator="between">
<nana-date-range clearable presets></nana-date-range>
</nana-filter-item>
</nana-filter> Columns
Section titled “Columns”A column picker belongs in the query but is not a filter — it does not narrow
the result set. Mark it no-chip and it stays out of the chip row, out of the
count, and out of Clear all, while still contributing its value.
<nana-filter label="Filters" variant="panel">
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
</nana-select>
</nana-filter-item>
<nana-filter-item name="columns" label="Visible columns" no-chip>
<nana-checkbox-group orientation="horizontal">
<nana-checkbox value="ref" checked>Ref</nana-checkbox>
<nana-checkbox value="customer" checked>Customer</nana-checkbox>
<nana-checkbox value="status" checked>Status</nana-checkbox>
<nana-checkbox value="total">Total</nana-checkbox>
</nana-checkbox-group>
</nana-filter-item>
</nana-filter> Variants
Section titled “Variants”bar is a bare wrapping row for sitting directly above a table. panel adds
card chrome and lays the items out in a responsive grid. popover puts the
whole panel behind a trigger button carrying the active count.
<nana-filter variant="panel" label="Filter orders">
<nana-filter-item name="q" label="Search" grow>
<nana-input placeholder="Search orders" clearable></nana-input>
</nana-filter-item>
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
</nana-select>
</nana-filter-item>
</nana-filter> <div style="min-height: 22rem">
<nana-filter variant="popover" label="Filters">
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
<nana-option value="closed">Closed</nana-option>
</nana-select>
</nana-filter-item>
<nana-filter-item name="created" label="Created" operator="between">
<nana-date-range clearable></nana-date-range>
</nana-filter-item>
</nana-filter>
</div> Live and manual modes
Section titled “Live and manual modes”live (the default) publishes on every field change. mode="manual" buffers
edits and adds an Apply / Reset footer — the right default when each change
costs a round trip. value always reflects what is in the fields; in manual
mode those changes simply are not published until Apply.
<nana-filter variant="panel" mode="manual" label="Filter orders">
<nana-filter-item name="q" label="Search" grow>
<nana-input placeholder="Search orders" clearable></nana-input>
</nana-filter-item>
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
</nana-select>
</nana-filter-item>
</nana-filter> <nana-filter size="sm" variant="panel" label="Small">
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all" size="sm">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
</nana-select>
</nana-filter-item>
</nana-filter> Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
applyLabel | apply-label | string | 'Apply' | |
clearLabel | clear-label | string | 'Clear all' | |
emptyText | empty-text | string | '' | Message shown in place of the chip row while nothing is filtered. Empty by default, which collapses the row entirely. |
label | — | string | 'Filters' | Heading for the `panel` and `popover` variants, and the default trigger's label. |
mode | — | FilterMode | 'live' | `live` publishes on every field change; `manual` holds changes back until Apply and shows a footer to commit them. |
noChips | no-chips | boolean | false | Hide the active-filter chip row. |
noClear | no-clear | boolean | false | Hide the clear-all control. |
open | — | boolean | false | Open state of the `popover` variant. |
placement | — | PopupPlacement | 'bottom-start' | Where the `popover` panel sits relative to its trigger. |
resetLabel | reset-label | string | 'Reset' | |
size | — | FilterSize | 'md' | |
value | — | Record<string, unknown> | — | The aggregated value — active filters only, keyed by item `name`. Always reflects what is currently in the fields, including in `manual` mode where those changes have not been published yet. Assigning writes each key through to its field and refreshes the chips without publishing, so restoring from a URL does not echo an event back. |
variant | — | FilterVariant | 'bar' | Layout: an inline toolbar, a bordered card, or a panel behind a button. |
Events
| Event | Description |
|---|---|
nana-filter-apply | Apply was activated (`detail`: `{ value, filters }`) |
nana-filter-change | The aggregated value changed. In `manual` mode this waits for Apply. (`detail`: `{ value, filters }`) |
nana-filter-clear | Every filter was cleared |
nana-filter-remove | A single filter was removed from the chip row (`detail`: `{ name }`) |
Slots
| Slot | Description |
|---|---|
(default) | One or more `nana-filter-item` elements |
trigger | Replaces the default button in the `popover` variant |
header-actions | Extra controls in the header (`panel` / `popover`) |
actions | Extra controls in the footer, beside Apply / Reset |
CSS parts
| Part | Description |
|---|---|
::part(base) | The filter surface |
::part(header) | The title row (`panel` / `popover`) |
::part(title) | The heading text |
::part(count) | The active-filter badge |
::part(body) | The container the filter items are slotted into |
::part(chips) | The active-filter chip row |
::part(chip) | One active-filter chip |
::part(clear) | The clear-all button |
::part(footer) | The Apply / Reset row (`manual` mode) |
::part(apply) | The apply button |
::part(reset) | The reset button |
::part(trigger) | The default popover trigger button |
::part(panel) | The floating panel (`popover` variant) |
CSS custom properties
| Property | Description |
|---|---|
--nana-filter-bg | Surface background (`panel` / `popover`) |
--nana-filter-border | Surface border colour |
--nana-filter-radius | Surface corner radius |
--nana-filter-gap | Gap between filter items |
--nana-filter-padding | Surface padding (`panel` / `popover`) |
--nana-filter-columns | Grid template for the item row |
--nana-filter-item-min-width | Basis a `grow` item flexes from before it wraps |
--nana-filter-panel-width | Minimum width of the floating panel (`popover` variant) |
Accessibility
Section titled “Accessibility”- The popover trigger carries
aria-haspopup="dialog"and a livearia-expanded; the panel is a labelleddialog. - Each
nana-filter-itemforwards itslabelto the slotted field as anaria-label, but only when that field has no accessible name of its own — so a field with its ownlabelkeeps it. - The active count is decorative (
aria-hidden); the chip row carries the same information as text. - Chips are
nana-tagelements with real remove buttons, so every filter can be dropped from the keyboard.