# A filter component for data tables, and a sharper tab strip

> Source: https://design.nanatec.co.ke/blog/filter-component-and-tab-polish/
> nana-filter and nana-filter-item collect any set of field components into one value with removable chips. Plus a polished segmented tab control, a new easing token, and a hover state that had been silently dropped.

<p style="color: var(--sl-color-gray-3); font-size: 0.9rem; margin-top: -0.5rem;">
  <time datetime="2026-08-17">August 17, 2026</time> · Release
</p>

This release adds the filter toolbar that sits above a data table, sharpens the
segmented tab control, and fixes a hover state that had never actually rendered.

## New: `nana-filter` and `nana-filter-item`

Filtering a table needs a search box, a status cut, a date window, and often a
column picker — four different controls that all have to end up in one query. The
new components collect them without replacing any of them.

Each filter is a `nana-filter-item` wrapping whichever field suits it. The values
are read by duck-typing (`value`, or `checked` for switches and checkboxes)
rather than by tag name, so a field this library doesn't ship works too, as long
as it exposes a value and fires `nana-change` or `change`.

```html
<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>
```

The aggregated value is a plain object keyed by each item's `name` — active
filters only:

```js
filter.addEventListener("nana-filter-change", (e) => {
  loadOrders(new URLSearchParams(e.detail.value)); // { q: "acme", status: "open" }
});

// Restoring from the URL writes through to every field, without echoing an event back.
filter.value = Object.fromEntries(new URLSearchParams(location.search));
```

### Three details worth knowing

**`default-value` is what stops a chip appearing immediately.** A select sitting
on **All** holds a non-empty value, so without it the filter counts as active
from first paint and shows a chip forever. Set it to whatever means "no filter"
and the item stays inactive until the user moves off it. `clear()` returns the
field there too.

**Chips resolve option labels.** A chip reads **Status is Open**, not
`status: open` — the item looks up the chosen `nana-option` and uses its text. A
date range's `start/end` is split back into two readable ends, and a multi-select
collapses into a single removable token rather than one chip per value.

**`no-chip` covers the things that aren't really filters.** A column picker
belongs in the query but doesn't narrow the result set, so rendering
"Columns: Ref, Customer, Total" as a removable chip is misleading. Mark it
`no-chip` and it contributes its value while staying out of the chip row, out of
the active count, and out of **Clear all** — nobody clearing their filters means
to reset which columns they can see.

```html
<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="total">Total</nana-checkbox>
    </nana-checkbox-group>
  </nana-filter-item>
</nana-filter>
```

Three layouts — `bar` for sitting directly above a table, `panel` for card chrome
and a responsive grid, `popover` for a trigger button carrying the active count.
`mode="manual"` buffers edits behind an Apply / Reset footer when each change
costs a round trip.

See [Filter](/components/filter/) and [Filter Item](/components/filter-item/).

## Things that change

**Segmented tab segments now sit flush.** The 0.25rem gap between them is gone.
A gap opened grey channels through the track, which read as several separate
controls rather than one strip with a moving thumb. The thumb's corner radius is
now derived from the track's radius minus its padding, so the two curves stay
concentric instead of the thumb looking pasted into a rounder hole. Both are
themeable via `--nana-tab-group-track-radius` and `--nana-tab-group-track-pad`.

**Disabled tabs recede by opacity, not by colour.** They previously used the
subtle text step; they now use the inactive colour at 45% opacity, so they stay
distinguishable from a merely unselected tab in both themes.

**Tab labels are fractionally tighter.** Tab text now carries the tight
letter-spacing token. Very wide tab strips may reflow by a pixel or two.

## Polish

The segmented control got a proper raised thumb: a three-layer shadow — a tight
contact shadow, a wider ambient one, and a hairline edge — that tightens as the
thumb compresses under a press. The track carries an inset hairline so it reads
as recessed rather than as a flat grey rectangle.

One thing we tried and backed out: pushing unselected labels further into the
background. Measured on the segmented track, the subtle text step gives
**3.98:1** at the 14px label size — under the 4.5:1 AA floor. The track is what
breaks it; on a bare page the same colour scrapes 4.74:1. So unselected labels
stay on the muted step (6.55:1 on the track), and selection is carried by the gap
to the active state instead — full-strength text on an opaque thumb at 17.9:1.
`--nana-tab-inactive-color` is there if you want to recess it against a
background you've checked yourself.

## Fixes

**`nana-input`'s hover border never rendered.** The `@media (hover: hover)` guard
trailed the selector instead of wrapping the rule:

```css
/* A selector followed by @media is not valid nesting — the whole block was dropped. */
:host(:not([disabled])) @media (hover: hover) { .wrapper:hover { … } }
```

Malformed CSS inside a Lit `` css`` `` template doesn't fail the TypeScript
build, so this sat there silently. The field now shows its border affordance on
hover before focus.

## New token

`--nana-transition-easing-emphasized` — `cubic-bezier(0.32, 0.72, 0, 1)`, a
decelerating curve for movement that should settle rather than stop dead. The tab
indicator and the segmented thumb both use it.

## A note on versions

**0.0.19 and 0.0.20 ship identical code.** The per-package release script was
named `publish`, which is also an npm lifecycle hook — so `npm publish` uploaded
the package and then invoked itself against the version it had just created. The
resulting `409 Conflict` made a successful release look like a failed one. The
script is renamed, and 0.0.20 is the version to depend on.
