Filter Bar
alphav0.2.0Composite filter bar — search, category pills, date-range picker, and results count, each independently controlled or hidden.
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
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/filter-barAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/filter-bar-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
42 items found
Demo source
"use client"; import { useState } from "react";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { FilterBar } from "./filter-bar";import { DUMMY_CATEGORIES_EN, DUMMY_LABELS_TR, formatDateRangeTR,} from "./dummy-data";import type { FilterBarValue } from "./types"; export default function FilterBarDemo() { const [controlledValue, setControlledValue] = useState<FilterBarValue>({ search: "", category: null, dateRange: { from: undefined, to: undefined }, }); return ( <Tabs defaultValue="basic" className="w-full"> <SwipeTabsList> <TabsTrigger value="basic">Basic</TabsTrigger> <TabsTrigger value="controlled">Controlled</TabsTrigger> <TabsTrigger value="partial">Partial usage</TabsTrigger> <TabsTrigger value="i18n">Localized</TabsTrigger> </SwipeTabsList> <TabsContent value="basic" className="mt-6"> <FilterBar categories={DUMMY_CATEGORIES_EN} resultsCount={42} /> </TabsContent> <TabsContent value="controlled" className="mt-6 space-y-4"> <FilterBar categories={DUMMY_CATEGORIES_EN} search={controlledValue.search} onSearchChange={(search) => setControlledValue((v) => ({ ...v, search })) } category={controlledValue.category} onCategoryChange={(category) => setControlledValue((v) => ({ ...v, category })) } dateRange={controlledValue.dateRange} onDateRangeChange={(dateRange) => setControlledValue((v) => ({ ...v, dateRange })) } resultsCount={42} /> <pre className="rounded-md border border-border bg-muted p-4 text-xs"> {JSON.stringify( { search: controlledValue.search, category: controlledValue.category, dateRange: { from: controlledValue.dateRange.from?.toISOString(), to: controlledValue.dateRange.to?.toISOString(), }, }, null, 2, )} </pre> </TabsContent> <TabsContent value="partial" className="mt-6 space-y-8"> <div> <p className="mb-2 text-sm font-semibold text-foreground"> Search + chips only (date hidden) </p> <FilterBar categories={DUMMY_CATEGORIES_EN} hideDateRange resultsCount={42} /> </div> <div> <p className="mb-2 text-sm font-semibold text-foreground"> Search + date only (categories hidden) </p> <FilterBar hideCategories resultsCount={42} /> </div> </TabsContent> <TabsContent value="i18n" className="mt-6"> <FilterBar categories={DUMMY_CATEGORIES_EN} labels={DUMMY_LABELS_TR} formatDateRange={formatDateRangeTR} resultsCount={12} /> </TabsContent> </Tabs> );} 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"witharia-label; each chip is a real<button>witharia-pressed. - Results count has
aria-live="polite"so updates announce. - Clear-date button has
aria-labelfromlabels.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