Skip to content
Nana UI

Filter

View .md Download

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.

All Open Pending Closed
Terminal window
npm install @nana-tec/ui-components
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.

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));

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.

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.

All Open Pending Closed

A segmented control reads faster when there are five or fewer statuses and they are the primary cut through the data:

All Open Pending Closed

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.

Open Pending Closed

A date range serialises as start/end. The chip splits it back into two readable ends rather than showing the raw value.

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.

All Open Ref Customer Status Total

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.

All Open
All Open Closed

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.

All Open
All Open

Properties

PropertyAttributeTypeDefaultDescription
applyLabelapply-labelstring'Apply'
clearLabelclear-labelstring'Clear all'
emptyTextempty-textstring''Message shown in place of the chip row while nothing is filtered. Empty by default, which collapses the row entirely.
labelstring'Filters'Heading for the `panel` and `popover` variants, and the default trigger's label.
modeFilterMode'live'`live` publishes on every field change; `manual` holds changes back until Apply and shows a footer to commit them.
noChipsno-chipsbooleanfalseHide the active-filter chip row.
noClearno-clearbooleanfalseHide the clear-all control.
openbooleanfalseOpen state of the `popover` variant.
placementPopupPlacement'bottom-start'Where the `popover` panel sits relative to its trigger.
resetLabelreset-labelstring'Reset'
sizeFilterSize'md'
valueRecord<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.
variantFilterVariant'bar'Layout: an inline toolbar, a bordered card, or a panel behind a button.

Events

EventDescription
nana-filter-applyApply was activated (`detail`: `{ value, filters }`)
nana-filter-changeThe aggregated value changed. In `manual` mode this waits for Apply. (`detail`: `{ value, filters }`)
nana-filter-clearEvery filter was cleared
nana-filter-removeA single filter was removed from the chip row (`detail`: `{ name }`)

Slots

SlotDescription
(default)One or more `nana-filter-item` elements
triggerReplaces the default button in the `popover` variant
header-actionsExtra controls in the header (`panel` / `popover`)
actionsExtra controls in the footer, beside Apply / Reset

CSS parts

PartDescription
::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

PropertyDescription
--nana-filter-bgSurface background (`panel` / `popover`)
--nana-filter-borderSurface border colour
--nana-filter-radiusSurface corner radius
--nana-filter-gapGap between filter items
--nana-filter-paddingSurface padding (`panel` / `popover`)
--nana-filter-columnsGrid template for the item row
--nana-filter-item-min-widthBasis a `grow` item flexes from before it wraps
--nana-filter-panel-widthMinimum width of the floating panel (`popover` variant)
  • The popover trigger carries aria-haspopup="dialog" and a live aria-expanded; the panel is a labelled dialog.
  • Each nana-filter-item forwards its label to the slotted field as an aria-label, but only when that field has no accessible name of its own — so a field with its own label keeps it.
  • The active count is decorative (aria-hidden); the chip row carries the same information as text.
  • Chips are nana-tag elements with real remove buttons, so every filter can be dropped from the keyboard.