Photon Design System

Filter Bar

The row that pins above a log list: a search field for an id, then one chip per filter. A chip with nothing chosen reads + Status; with a value it reads Status: Failed and carries its own clear. Each chip opens a menu, a keyboard-first list of options or presets and a from / to pair for a date range. The bar is built from the library’s own controls: a chip is an outlined Button, and what it opens is a Menu, so it wears the same hairline, shadow and entrance as every other menu in both themes. The bar is fully controlled: the page owns the query and hands it back changed, so the list can refetch from one place (the URL, a store).

Query: {"search":"","status":"failed"}

Import

import { FilterBar } from "@photon-ai/pho-ui/components/filter-bar";

Definitions

definitions describes the chips, in order. A select filter lists options (value and label); multiple lets several be chosen at once, and past eight options the menu grows a search field (searchable says so either way). A long option (a route, a path) widens the menu up to a cap and truncates past it, with the whole text on hover. A date-range filter (type: "date-range") lists relative presets (last hour, 24 hours, 7 days, 30 days unless told otherwise) over a from / to pair.

const definitions: FilterDefinition[] = [
  { key: "date", label: "Date", type: "date-range" },
  {
    key: "status",
    label: "Status",
    options: [
      { value: "succeeded", label: "Succeeded" },
      { value: "failed", label: "Failed" },
    ],
  },
  {
    key: "type",
    label: "Event type",
    multiple: true,
    options: EVENT_TYPES,
  },
];

Values

filters is a map from definition key to value: a string for a select, string[] when multiple, and { from, to, preset? } for a date range. An absent key, an empty string, an empty list, or a range with both sides open all count as unset. onFiltersChange receives the whole map; keys the bar does not own pass through untouched. A preset stores an absolute ISO timestamp in from and its label in preset; the from / to fields store plain YYYY-MM-DD days.

const [filters, setFilters] = useState<FilterValues>({});
const [search, setSearch] = useState("");

<FilterBar
  definitions={definitions}
  filters={filters}
  onFiltersChange={setFilters}
  search={search}
  onSearchChange={setSearch}
  searchPlaceholder="Find an event by id"
/>;

Pinning above a list

The bar has no scroll container of its own; chips wrap. sticky pins it to the top of the nearest scroll pane and paints the surface it sits on (--pho-surface), so rows scroll under it. Put it directly above the LogList in the same pane.

200GET
40 ms2 min ago
200POST
43 ms5 min ago
200POST
46 ms12 min ago
500GET
49 ms1 h ago
200POST
52 ms3 h ago
200POST
55 ms2 min ago
200GET
58 ms5 min ago
200POST
61 ms12 min ago
200POST
64 ms1 h ago
200GET
67 ms3 h ago
500POST
70 ms2 min ago
200POST
73 ms5 min ago
200GET
76 ms12 min ago
200POST
79 ms1 h ago
200POST
82 ms3 h ago
200GET
85 ms2 min ago
200POST
88 ms5 min ago
500POST
91 ms12 min ago
200GET
94 ms1 h ago
200POST
97 ms3 h ago
200POST
100 ms2 min ago
200GET
103 ms5 min ago
200POST
106 ms12 min ago
200POST
109 ms1 h ago
<div className="overflow-y-auto">
  <FilterBar sticky … />
  <LogList.Root>…</LogList.Root>
</div>

Props

  • definitions — FilterDefinition[] — one chip per entry, in order
  • filters — FilterValues — the values by key; absent or empty means unset
  • onFiltersChange — (next: FilterValues) => void — the whole map, changed
  • search / onSearchChange — string / (value: string) => void — the field renders only with onSearchChange; Escape clears it
  • searchPlaceholder — string (default Search) — a teaching phrase, “Find an event by id”
  • searchLabel — string — the field’s accessible name when the placeholder is too short to be one
  • sticky — boolean — pin to the top of the nearest scroll pane
  • children — controls after the chips (an export button)
  • …plus any <div> attribute

Helpers: isFilterSet(value), formatFilterValue(definition, value) (the words a set chip shows), and FILTER_DATE_PRESETS.

Accessibility

  • Chips are buttons. The trigger opens a menu (aria-haspopup, aria-expanded); the clear beside it is its own button, named Clear status filter. An unset chip is named Status filter; a set one reads its visible words, Status: Failed.
  • The menu is keyboard-first. Focus lands on the search field when there is one, else on the rows; arrows move, Enter picks, Escape closes back onto the chip. Options are menuitemradio rows (or menuitemcheckbox when multiple) and carry aria-checked.
  • Date fields are native. The from / to pair are <input type="date"> with visible labels; they stand outside the preset list so the arrow keys step dates as usual.

Best practices

  • Keep chip labels to a word or two in sentence case: Status, Event type.
  • Sort options the way the user thinks, and let multiple carry the cases where a user wants two states at once (failed and pending).
  • Send the filter state to the server as it is; the bar formats nothing but the chip. Parse from / to with new Date() on either side.
  • One bar per list, pinned above it. A second row of controls goes in children, after the chips.