Skip to content
Nana UI

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.

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

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.
labellabelstring'Filters'Heading for the panel and popover variants, and the default trigger's label.
modemodeFilterMode'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.
openopenbooleanfalseOpen state of the popover variant.
placementplacementPopupPlacement'bottom-start'Where the popover panel sits relative to its trigger.
resetLabelreset-labelstring'Reset'
sizesizeFilterSize'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.
variantvariantFilterVariant'bar'Layout: an inline toolbar, a bordered card, or a panel behind a button.

Events

EventDescription
nana-filter-changeThe aggregated value changed. In manual mode this waits for Apply. (detail: { value, filters })
nana-filter-applyApply was activated (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(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

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.
Explore Filter interactively Every state, prop, and edge case — with live controls — in Storybook.
View in Storybook