Skip to content
ilinxa/pro-ui

Filter Panel

alphav0.2.0

Schema-driven filter panel — checkbox lists, toggles, text, and custom filter types with AND composition and debounced input.

Category: FormsUpdated: 2026-08-11Created: 2026-04-29Author: ilinxa

Context

Tier 1 pro-component for the graph-system. Generic over the item type; the host supplies items, categories, predicates, and decides what to do with the filtered output. AND-across-categories with OR-or-AND-within-category controlled by the host's predicate. Pairs with the graph in force-graph v0.4 for the groups/filter panel; useful standalone wherever a faceted-filter sidebar is needed.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/filter-panel

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/filter-panel-fixtures

Preview

Kind
Search by name
Pinned only
Off
  • Rina Okaforperson
    securityauth
  • Marc Bernalperson
    frontend
  • Acme Corporg
    customertier-1
  • Verda Holdingsorg
    customer
  • auth-v2 migrationproject
    securityauthtier-1
  • registry publishproject
    frontendops
  • v2-runbook.mddoc
    securityauth
  • design-tokens.mddoc
    frontend

Demo source

demo.tsxtsx

Usage

When to use

Reach for FilterPanel when you have a list of typed items and want a stacked filter panel — checkbox lists, toggles, debounced text, custom range pickers — composing AND-across categories. Generic over the item type; the host owns items, predicates, and what to do with the filtered output. Sync-only predicates in v0.1; async slot is a v0.2 additive prop.

Basic example

import {
  FilterPanel,
  type FilterCategory,
} from "@/components/filter-panel";

interface Project {
  id: string;
  name: string;
  status: "todo" | "in-progress" | "done";
  tags: string[];
}

const CATEGORIES: ReadonlyArray<FilterCategory<Project>> = [
  {
    id: "status",
    type: "checkbox-list",
    label: "Status",
    options: [
      { value: "todo", label: "To do" },
      { value: "in-progress", label: "In progress" },
      { value: "done", label: "Done" },
    ],
    predicate: (item, value) => {
      const sel = (value as string[]) ?? [];
      return sel.length === 0 || sel.includes(item.status);
    },
  },
  {
    id: "search",
    type: "text",
    label: "Search",
    placeholder: "Filter by name…",
    predicate: (item, value) =>
      typeof value !== "string" || value.length === 0 ||
      item.name.toLowerCase().includes(value.toLowerCase()),
  },
];

export function ProjectFilters({ projects }: { projects: Project[] }) {
  const [values, setValues] = useState<Record<string, unknown>>({});
  return (
    <FilterPanel<Project>
      items={projects}
      categories={CATEGORIES}
      values={values}
      onChange={setValues}
      onFilteredChange={setFilteredProjects}
    />
  );
}

Filter types

  • checkbox-list — multi-select. Optional modeToggle for Union / Intersection. Optional showSoloButtons for per-row solo affordance.
  • toggle — boolean switch. isEmpty required (host intent governs — typically (v) => v !== true).
  • text — debounced input. debounceMs defaults to 250. ESC clears the field.
  • custom — escape hatch. render(props) receives { value, onChange, items, fieldId }; isEmpty required.

Composition semantics

AND-across categories: every active category's predicate must return true. Within a category, the host decides OR vs AND inside predicate. The mode-toggle affordance for checkbox-list is a hint stored at values["${id}__mode"] (reserved suffix); your predicate reads it and switches.

Empty detection

A category is "empty" when its isEmpty(value) returns true; empty categories are skipped during filtering. Defaults: checkbox-list = empty array; text = empty string. Required for toggle and custom because only the host knows the value shape.

Imperative handle

const ref = useRef<FilterPanelHandle>(null);
// ...
ref.current?.clearAll();
ref.current?.clear("status");
ref.current?.isEmpty();   // true iff every category is empty

Categories reference stability

Inline categories={[...]} rebuilds category objects on every parent render and re-runs the filter pipeline. In-repo, the React Compiler memoizes inline literals at the call site. For NPM consumers without the Compiler, hoist to module scope or wrap with useMemo.

const CATEGORIES: FilterCategory<Item>[] = [/* ... */];
// or
const categories = useMemo(() => buildCategories(...), [deps]);

onFilteredChange semantics

Fires when the filtered array's reference changes — "may have changed", not "definitely differs". Same items + same values + same categories → no fire. Cost-conscious hosts dedupe via shallow-equal-by-id on their side.

Reserved id suffixes

__mode is reserved for internal mode storage on checkbox-list categories with modeToggle: true. Schema validation (dev-only) flags category ids ending in reserved suffixes, duplicate ids, and checkbox-list categories with empty options.

What ships in v0.2+

  • Built-in range / date-range filter types.
  • Per-category collapsibles (collapsible + defaultExpanded).
  • Horizontal layout via direction prop.
  • Async predicate support.
  • Deep-equal change detection for onFilteredChange.

Features

  • Four built-in filter types — checkbox-list, toggle, text, custom
  • AND-across-categories composition; host-defined within-category semantics
  • Mode toggle (Union / Intersection) on checkbox-list via a plain-button segmented control
  • Per-option solo button with tooltip on checkbox-list
  • Debounced text input (default 250ms) with flush-on-blur, ESC-clears
  • Custom render slot with error boundary and label-association via fieldId
  • Per-category clear button + global clear-all in footer
  • Dev-only schema validation — reserved suffix, duplicate id, empty options
  • Categories-reference-instability dev warning (>5 successive unstable renders)
  • Imperative handle — clearAll / clear / isEmpty

Tags

filter-panelfilterfacetsgraph-system

Dependencies

shadcn primitives: button, checkbox, input, switch, tooltip
npm peer deps: lucide-react@^1.11.0