Componentform1.53 KB gzipZero dependencies
Search Filter Bar
Combined search, sort and filter toolbar with animated chips.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add search-filter-barOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { SearchFilterBar } from "@/components/ui/search-filter-bar";
<SearchFilterBar />Usage guidance
Use Search Filter Bar when a ticket stub surface needs Combined search, sort and filter toolbar with animated chips. Keep props minimal and prefer composition with sibling stubs.
Avoid Search Filter Bar for long form flows or dense data grids where a dedicated layout block fits better.
Source code
search-filter-bar.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface SearchFilterBarProps extends React.HTMLAttributes<HTMLDivElement> {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
sorts?: { id: string; label: string }[];
activeSort?: string;
onSortChange?: (id: string) => void;
filters?: { id: string; label: string; active?: boolean }[];
onFilterToggle?: (id: string) => void;
}
/** Search and filter toolbar: search input + sort dropdown + filters. */
export function SearchFilterBar({
value,
defaultValue,
onValueChange,
placeholder = "Ara…",
sorts = [],
activeSort,
onSortChange,
filters = [],
onFilterToggle,
className,
...props
}: SearchFilterBarProps) {
const [internal, setInternal] = React.useState(defaultValue ?? "");
const query = value ?? internal;
const [activeFilters, setActiveFilters] = React.useState<Set<string>>(
() => new Set(filters.filter((f) => f.active).map((f) => f.id))
);
React.useEffect(() => {
setActiveFilters(new Set(filters.filter((f) => f.active).map((f) => f.id)));
}, [filters]);
const setQuery = (v: string) => { setInternal(v); onValueChange?.(v); };
const toggleFilter = (id: string) => {
setActiveFilters((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
onFilterToggle?.(id);
};
return (
<div className={cn("flex flex-wrap items-center gap-2", className)} role="search" {...props}>
<style>{`@keyframes sfbChip { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }`}</style>
{/* Arama */}
<div className="relative min-w-40 flex-1">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.35-4.35" />
</svg>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
aria-label={placeholder}
className="w-full rounded-md border border-input bg-background py-1.5 ps-8 pe-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors motion-reduce:transition-none"
/>
</div>
{/* Sort */}
{sorts.length > 0 && (
<div className="flex gap-1 rounded-md border border-border bg-card p-0.5" role="radiogroup" aria-label="Sorting options">
{sorts.map((s) => (
<button
key={s.id}
role="radio"
aria-checked={activeSort === s.id}
onClick={() => onSortChange?.(s.id)}
className={cn(
"rounded-sm px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
activeSort === s.id ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:text-foreground"
)}
>
{s.label}
</button>
))}
</div>
)}
{/* Filtre chip'leri */}
{filters.map((f) => (
<button
key={f.id}
onClick={() => toggleFilter(f.id)}
aria-pressed={activeFilters.has(f.id)}
className={cn(
"rounded-full border px-2.5 py-1 font-mono text-[10px] uppercase tracking-wider transition-all duration-150 motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"animate-[sfbChip_0.2s_ease-out] motion-reduce:animate-none",
activeFilters.has(f.id)
? "border-accent bg-accent/10 text-accent"
: "border-border text-muted-foreground hover:border-foreground/40 hover:text-foreground"
)}
>
{f.label}
</button>
))}
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| value | string | |
| defaultValue | string | |
| onValueChange | (value: string) => void | |
| placeholder | string | |
| sorts | { id: string; label: string }[] | |
| activeSort | string | |
| onSortChange | (id: string) => void | |
| filters | { id: string; label: string; active?: boolean }[] | |
| onFilterToggle | (id: string) => void |