bigbullui logobigbullui
Componentform

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

Or install the full npm package:

npm install bigbullui

Usage

import { SearchFilterBar } from "@/components/ui/search-filter-bar";

<SearchFilterBar />

Source code

search-filter-bar.tsx

Zero 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;
}

/** Arama + sıralama + filtre birleşik araç çubuğu. */
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 pl-8 pr-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors motion-reduce:transition-none"
        />
      </div>

      {/* Sıralama */}
      {sorts.length > 0 && (
        <div className="flex gap-1 rounded-md border border-border bg-card p-0.5" role="radiogroup" aria-label="Sıralama">
          {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

PropTypeDescription
valuestring
defaultValuestring
onValueChange(value: string) => void
placeholderstring
sorts{ id: string; label: string }[]
activeSortstring
onSortChange(id: string) => void
filters{ id: string; label: string; active?: boolean }[]
onFilterToggle(id: string) => void