Data Display & Media
Filter
Collects a set of filter fields into one value, renders the active ones as removable chips, and publishes the result — the toolbar that sits above a data table.
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; 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:
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 | label | string | 'Filters' | Heading for the panel and popover variants, and the default trigger's label. |
mode | 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 | open | boolean | false | Open state of the popover variant. |
placement | placement | PopupPlacement | 'bottom-start' | Where the popover panel sits relative to its trigger. |
resetLabel | reset-label | string | 'Reset' | |
size | 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 | variant | FilterVariant | 'bar' | Layout: an inline toolbar, a bordered card, or a panel behind a button. |
Events
| Event | Description |
|---|---|
nana-filter-change | The aggregated value changed. In manual mode this waits for Apply. (detail: { value, filters }) |
nana-filter-apply | Apply was activated (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(popup) | The popover surface, forwarded from nana-popup |
::part(arrow) | The popover arrow, forwarded from nana-popup |
::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.