Filter

Segmented filter chips, a faceted filter menu, and removable tokens for the current selection.

Filter controls for toolbars and list screens. The caller owns the selection — these components only render it and report changes.

StatusLive

Segmented bar

FilterBar is a single-choice track, the same shape as a segmented tab list. Arrow keys move the selection.

tsx
import { FilterBar, FilterChip } from "@celestia-project/ui/composite/filter"
export function ScopeFilter({  scope,  onScopeChange,}: {  scope: string  onScopeChange: (scope: string) => void}) {  return (    <FilterBar aria-label="Issue scope">      <FilterChip selected={scope === "all"} onClick={() => onScopeChange("all")}>        All      </FilterChip>      <FilterChip selected={scope === "open"} onClick={() => onScopeChange("open")}>        Open      </FilterChip>      <FilterChip selected={scope === "done"} onClick={() => onScopeChange("done")}>        Done      </FilterChip>    </FilterBar>  )}

A FilterChip used outside a bar is a toggle pill (aria-pressed) rather than a segment.

Faceted menu

FilterMenu is the "Status" control on a list toolbar. multiple toggles options and stays open. single keeps one value and closes.

tsx
import { useState } from "react"import { FilterMenu } from "@celestia-project/ui/composite/filter"
const STATUS = [  { value: "live", label: "Live", count: 12 },  { value: "building", label: "Building", count: 4 },  { value: "draft", label: "Draft", count: 3 },]
export function StatusFilter() {  const [status, setStatus] = useState<string[]>([])
  return (    <FilterMenu      label="Status"      options={STATUS}      selected={status}      onSelectedChange={setStatus}    />  )}

Search appears on its own once there are more than eight options. Set searchable to force it on or off.

Active tokens

FilterToken restates a choice next to the menu. FilterClear drops every choice at once.

tsx
import { FilterClear, FilterToken } from "@celestia-project/ui/composite/filter"
export function ActiveStatus({  status,  onRemove,  onClear,}: {  status: { value: string; label: string }[]  onRemove: (value: string) => void  onClear: () => void}) {  if (status.length === 0) return null
  return (    <>      {status.map((option) => (        <FilterToken          key={option.value}          label="Status"          value={option.label}          onRemove={() => onRemove(option.value)}        />      ))}      <FilterClear onClick={onClear} />    </>  )}