Skip to content

Index filters with saved views

components specs/components/index-filters.kmd

Composite search + filter chips + saved views companion of data-table — handles full-text search, faceted filtering, multi- condition combination, URL serialization, persisted named views. Modeled after Polaris IndexFilters.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Index filters

Status: v0.1.0 — Draft. Sits in the toolbar above specs/components/data-table.kmd.

R1 — Layout

Single horizontal row at the top of the data-table container:

SectionWidthContent
Search boxflex 1Magnifying-glass icon + input + clear (×)
Filter chip areaflex 2Active filter chips inline, wraps on overflow
Saved-views dropdownautoCaret + view name (default: Default)
"Add filter" buttonauto+ icon + label, opens filter picker popover

R2 — Filter chip anatomy

Per active filter:

  • Label: filter name (Status)
  • Operator: optional (is, is not, contains)
  • Value: human-readable (Active, Active, Pending for multi)
  • Dismiss (×): removes filter immediately

Chip background: --kdr-surface-2; text: --kdr-text; dismiss icon: --kdr-text-muted.

R3 — Multi-condition expression

  • Multiple filters compose with AND by default.
  • Each filter MAY have OR within its own values (Status: Active OR Pending).
  • Explicit grouping (parentheses) NOT supported in v0 — covered by future advanced-query ticket if demand surfaces.

R4 — Saved views

  • "View" = name + columns + sort + filter set.
  • Each view persisted per-user + per-table-identity (same scope as data-table saved views).
  • View dropdown shows: Default (always present, locked) + user-created views + "Save current as new view" CTA.
  • Renaming, deleting, sharing per view: hover menu on each entry.
  • Sharing across users not in v0.

R5 — URL serialization

  • Active filters MUST reflect in the URL querystring as ?filter[Status]=Active,Pending&filter[Date]=>2026-01-01.
  • Refreshing the page restores the same filter state.
  • Saved-view selection appears as ?view=my-open-issues; loading a view applies its filters (overriding any explicit filter params).

R6 — Keyboard navigation

KeyAction
Cmd+F / Ctrl+FFocus the search box
Esc (in search)Clear search and blur
TabMove through search, chips, dropdown, Add Filter
Enter on chip dismissRemove chip
Enter on Add FilterOpen filter picker
Esc (in picker)Close picker without applying

R7 — Internationalization

  • All strings (placeholder, operators, button labels) translatable per specs/i18n/contract.kmd.
  • Default-view name "Default" translates per locale.
  • Date-typed filter values use locale-aware formatting (per specs/i18n/contract.kmd date-time section).

R8 — Accessibility

  • Search box: <input type="search"> with associated label.
  • Chip set: role="list" with each chip role="listitem"; dismiss is a button inside.
  • Add-filter popover: role="dialog" with focus trap; Esc closes.
  • Live-region announces filter changes ("Filter added: Status is Active").

R9 — Empty state interaction

When current filter set returns 0 rows: data-table shows empty-state (per specs/patterns/empty-state.kmd) with a Clear filters secondary action.

Não-escopo

  • Sharing saved views across users (separate ticket if pursued).
  • Advanced query expression with explicit grouping (separate ticket).
  • Server-side filter execution (transport-agnostic).
  • Pro-layer bundling (this primitive composed with data-table + density toggle + column-visibility menu + export into a single drop-in component) — evaluated in design-RFC-008 and ratified Option C (2026-05-23) — the composition pattern ships as specs/patterns/admin-data-table.kmd rather than a bundled component.

Requirements (testable)

Requirement: Filter chip anatomy and dismissal {#req-index-filters-chip}

Each active filter SHALL render as a chip showing its label and human-readable value (operator optional), and each chip SHALL expose a dismiss (×) control that removes that filter immediately.

Scenario: Multi-value filter renders combined value

  • GIVEN an index-filters toolbar with a Status filter set to Active and Pending
  • WHEN the chip set renders
  • THEN a chip labeled Status is present
  • AND its value text reads Active, Pending

Scenario: Dismissing a chip removes the filter

  • GIVEN an active Status chip in the filter chip area
  • WHEN the user activates the chip's dismiss (×) control
  • THEN the Status filter is removed from the active filter set
  • AND the Status chip is no longer present in the chip set

Requirement: Multi-condition composition {#req-index-filters-multi-condition}

Multiple active filters SHALL compose with AND by default, and a single filter MAY combine its own values with OR, while explicit parenthetical grouping SHALL NOT be supported in v0.

Scenario: Two filters compose with AND

  • GIVEN a Status: Active filter and a Date: >2026-01-01 filter both active
  • WHEN the effective filter expression is evaluated
  • THEN the expression combines the two filters with AND
  • AND only rows matching both conditions are retained

Scenario: Single filter combines values with OR

  • GIVEN a Status filter with values Active and Pending
  • WHEN the effective filter expression is evaluated
  • THEN the Status condition matches rows where status is Active OR Pending

Requirement: URL serialization and restoration {#req-index-filters-url}

Active filters SHALL be reflected in the URL querystring as filter[<Name>]=<values>, and reloading the page SHALL restore the identical filter state; a view=<slug> parameter SHALL apply that saved view, overriding any explicit filter params.

Scenario: Applying a filter updates the querystring

  • GIVEN an index-filters toolbar with no active filters
  • WHEN the user applies Status = Active, Pending
  • THEN the URL querystring contains filter[Status]=Active,Pending

Scenario: Reloading restores filter state

  • GIVEN a URL whose querystring is ?filter[Status]=Active,Pending
  • WHEN the page is loaded
  • THEN the Status filter is active with values Active and Pending

Scenario: A saved view overrides explicit filter params

  • GIVEN a URL querystring ?filter[Status]=Active&view=my-open-issues
  • WHEN the page is loaded
  • THEN the my-open-issues saved view's filters are applied
  • AND the explicit filter[Status]=Active param does not add a separate active filter

Requirement: Saved views {#req-index-filters-saved-views}

The saved-views dropdown SHALL always present a locked Default view alongside the user's own views plus a "Save current as new view" action, and each view SHALL persist its name, columns, sort, and filter set per-user and per-table-identity.

Scenario: Default view is always present and locked

  • GIVEN a table for which the user has created no views
  • WHEN the saved-views dropdown is opened
  • THEN a Default view entry is shown
  • AND the Default entry offers no delete or rename action

Scenario: Saving the current state creates a named view

  • GIVEN active filters and a selected sort with no matching saved view
  • WHEN the user invokes "Save current as new view" and names it my-open-issues
  • THEN a my-open-issues view entry appears in the dropdown
  • AND selecting it re-applies the same filter set, columns, and sort

Requirement: Keyboard navigation {#req-index-filters-keyboard}

The toolbar SHALL support keyboard operation: Cmd/Ctrl+F SHALL focus the search box, Esc within the search box SHALL clear and blur it, and Esc within the filter picker SHALL close it without applying.

  • GIVEN an index-filters toolbar rendered with focus outside the search box
  • WHEN the user presses Ctrl+F (or Cmd+F)
  • THEN keyboard focus moves to the search input

Scenario: Esc closes the filter picker without applying

  • GIVEN the Add-filter picker popover is open with a pending unapplied selection
  • WHEN the user presses Esc
  • THEN the picker closes
  • AND no new filter is added to the active filter set

Requirement: Accessibility semantics {#req-index-filters-a11y}

The toolbar SHALL expose accessible semantics: the search box SHALL be a labeled <input type="search">, the chip set SHALL use role="list" with each chip role="listitem" and a button dismiss, the Add-filter popover SHALL be a role="dialog" with a focus trap closable by Esc, and filter changes SHALL be announced via a live region.

Scenario: Chip set and search expose correct roles

  • GIVEN an index-filters toolbar with at least one active chip
  • WHEN the accessibility tree is inspected
  • THEN the search input has type search with an associated label
  • AND the chip container has role="list" and each chip has role="listitem" containing a button dismiss control

Scenario: Adding a filter is announced and the popover traps focus

  • GIVEN the Add-filter popover is open
  • WHEN the user applies Status is Active
  • THEN while open the popover has role="dialog" and confines focus to its contents
  • AND a live region announces Filter added: Status is Active

References