Skip to content
ilinxa/pro-ui

Filter Bar

alphav0.2.0

Composite filter bar — search, category pills, date-range picker, and results count, each independently controlled or hidden.

Category: FormsUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

Browse-and-filter UI workhorse: news, blogs, docs, dashboards, e-commerce facets. Three sub-controls (search / chips / date-range) with independent state, plus a combined onChange emitting `{ search, category, dateRange }`. Search debounces 250ms in uncontrolled mode. Date-range Popover composes shadcn Calendar (which transitively brings react-day-picker + date-fns into the project — first user). Migration origin: kasder kas-social-front-v0 NewsMagazineGrid.tsx header section. Composed by `magazine-layout` in the news-domain family.

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-bar

Add -fixtures for dummy data:

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

Preview

42 items found

Demo source

demo.tsxtsx

Usage

When to use

Reach for FilterBar in any browse-and-filter UI: news landing pages, blog archives, doc indexes, file browsers, dashboards, e-commerce category pages. Three sub-controls in one bar — search + category chips + date-range — plus an optional results count. Each is independently controlled-or-uncontrolled.

Minimal example

import { FilterBar } from "@/components/filter-bar";

<FilterBar
  categories={[
    { value: "tech", label: "Technology" },
    { value: "design", label: "Design" },
  ]}
  resultsCount={42}
/>;

With no controlled props, all 3 sub-controls run uncontrolled with a 250ms debounce on the search. The bar emits no events but the UI works for visual demos.

Controlled (the common case)

const [filters, setFilters] = useState<FilterBarValue>({
  search: "",
  category: null,
  dateRange: { from: undefined, to: undefined },
});

<FilterBar
  categories={categories}
  search={filters.search}
  onSearchChange={(s) => setFilters((v) => ({ ...v, search: s }))}
  category={filters.category}
  onCategoryChange={(c) => setFilters((v) => ({ ...v, category: c }))}
  dateRange={filters.dateRange}
  onDateRangeChange={(d) => setFilters((v) => ({ ...v, dateRange: d }))}
  resultsCount={filteredItems.length}
/>;

Or use the combined onChange emitter:

<FilterBar
  categories={categories}
  onChange={({ search, category, dateRange }) => {
    setFilters({ search, category, dateRange });
  }}
/>;

Hide sub-controls

<FilterBar categories={categories} hideDateRange />
<FilterBar hideCategories hideSearch />

Localization

<FilterBar
  categories={categories}
  labels={{
    searchPlaceholder: "Haber ara...",
    allLabel: "Tümü",
    dateButtonText: "Tarih Filtrele",
    clearDateText: "Tarihi Temizle",
    resultsCountText: (n) => `${n} haber bulundu`,
  }}
  formatDateRange={({ from, to }) => {
    const fmt = new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "short" });
    return `${fmt.format(from)} - ${fmt.format(to)}`;
  }}
/>;

Search debounce

In uncontrolled mode, search is debounced 250ms by default — set searchDebounceMs={0} for instant or increase for slower-changing dropdowns. In controlled mode the debounce is bypassed; consumer's onChange fires on every keystroke. Debounce yourself if you need it.

Accessibility

  • Search input is wrapped in <div role="search"> for landmark.
  • Chip row uses role="group" with aria-label; each chip is a real <button> with aria-pressed.
  • Results count has aria-live="polite" so updates announce.
  • Clear-date button has aria-label from labels.clearDateLabel.
  • Date Popover inherits ARIA from shadcn primitives.

Features

  • 3 sub-controls in one bar — search input + category pill row + date-range Popover
  • Independently controlled-or-uncontrolled per sub-control
  • Combined `onChange` emits `{ search, category, dateRange }` on any change
  • Internal 250ms debounce on search in uncontrolled mode (configurable)
  • Sub-control hide flags — `hideSearch` / `hideCategories` / `hideDateRange` for partial usage
  • "All" sentinel chip clears the category filter (maps to null internally)
  • Optional results count with `aria-live='polite'` announcements
  • i18n via `labels` prop + `formatDate` / `formatDateRange` callbacks (English defaults; Turkish in dummy-data)
  • Layout `align` prop (left | center | right; default center)
  • Native `<button aria-pressed>` chips; `role=group` on chip row; `role=search` on search input
  • Date-range uses shadcn Popover + Calendar primitives (react-day-picker; date-fns transitive)
  • React.memo wrapped

Tags

filter-barformsfiltersearchdate-rangemigration

Dependencies

shadcn primitives: input, button, popover, calendar
npm peer deps: lucide-react@^1.11.0