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
- Pair filters with a Koder data-table surface
All triggers
- Add search + filter to an admin data table
- Replace ad-hoc filter UI with the canonical pattern
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:
| Section | Width | Content |
|---|---|---|
| Search box | flex 1 | Magnifying-glass icon + input + clear (×) |
| Filter chip area | flex 2 | Active filter chips inline, wraps on overflow |
| Saved-views dropdown | auto | Caret + view name (default: Default) |
| "Add filter" button | auto | + 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, Pendingfor 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
| Key | Action |
|---|---|
| Cmd+F / Ctrl+F | Focus the search box |
| Esc (in search) | Clear search and blur |
| Tab | Move through search, chips, dropdown, Add Filter |
| Enter on chip dismiss | Remove chip |
| Enter on Add Filter | Open 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.kmddate-time section).
R8 — Accessibility
- Search box:
<input type="search">with associated label. - Chip set:
role="list"with each chiprole="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 asspecs/patterns/admin-data-table.kmdrather 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
Statusfilter set toActiveandPending - WHEN the chip set renders
- THEN a chip labeled
Statusis present - AND its value text reads
Active, Pending
Scenario: Dismissing a chip removes the filter
- GIVEN an active
Statuschip in the filter chip area - WHEN the user activates the chip's dismiss (×) control
- THEN the
Statusfilter is removed from the active filter set - AND the
Statuschip 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: Activefilter and aDate: >2026-01-01filter 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
Statusfilter with valuesActiveandPending - WHEN the effective filter expression is evaluated
- THEN the
Statuscondition matches rows where status isActiveORPending
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
Statusfilter is active with valuesActiveandPending
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-issuessaved view's filters are applied - AND the explicit
filter[Status]=Activeparam 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
Defaultview entry is shown - AND the
Defaultentry 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-issuesview 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.
Scenario: Shortcut focuses the search box
- 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
searchwith an associated label - AND the chip container has
role="list"and each chip hasrole="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
specs/components/data-table.kmdspecs/patterns/admin-data-table.kmdspecs/patterns/empty-state.kmdspecs/i18n/contract.kmdrfcs/design-RFC-008-pro-opinionated-wrappers.kmd