1
Patterns

Filter Bar

Search and filters above a table, with active-filter badges and a clear action.

Search and active filters

Active
import { useState } from "react";
import {
  FilterBar,
  FilterBarActions,
  FilterBarActiveFilters,
  FilterBarControls,
  FilterBarFilters,
  FilterBarSearch,
} from "@/components/patterns/filter-bar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Menu,
  MenuCheckboxItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/ui/menu";
import { SearchIcon, SlidersHorizontalIcon, XIcon } from "lucide-react";

const statusOptions = ["Active", "Trial"] as const;

export function Example() {
  const [query, setQuery] = useState("");
  const [activeStatuses, setActiveStatuses] = useState<Set<string>>(
    () => new Set(["Active"]),
  );
  const hasActiveFilters = Boolean(query || activeStatuses.size);

  function setStatus(status: string, checked: boolean) {
    setActiveStatuses((current) => {
      const next = new Set(current);

      if (checked) {
        next.add(status);
      } else {
        next.delete(status);
      }

      return next;
    });
  }

  function clearFilters() {
    setQuery("");
    setActiveStatuses(new Set());
  }

  return (
    <FilterBar className="w-full max-w-2xl">
      <FilterBarControls>
        <FilterBarSearch>
          <div className="relative">
            <SearchIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
            <Input
              aria-label="Search customers"
              className="pl-8"
              onChange={(event) => setQuery(event.target.value)}
              placeholder="Search customers"
              type="search"
              value={query}
            />
          </div>
        </FilterBarSearch>
        <FilterBarFilters>
          <Menu>
            <MenuTrigger render={<Button type="button" variant="outline" />}>
              <SlidersHorizontalIcon />
              Status
            </MenuTrigger>
            <MenuPopup align="end">
              {statusOptions.map((status) => (
                <MenuCheckboxItem
                  checked={activeStatuses.has(status)}
                  key={status}
                  onCheckedChange={(checked) => setStatus(status, checked)}
                >
                  {status}
                </MenuCheckboxItem>
              ))}
            </MenuPopup>
          </Menu>
          {hasActiveFilters ? (
            <FilterBarActions>
              <Button onClick={clearFilters} type="button" variant="ghost">
                <XIcon />
                Clear
              </Button>
            </FilterBarActions>
          ) : null}
        </FilterBarFilters>
      </FilterBarControls>
      {hasActiveFilters ? (
        <FilterBarActiveFilters>
          {query ? <Badge variant="outline">Search: {query}</Badge> : null}
          {Array.from(activeStatuses).map((status) => (
            <Badge key={status} variant="secondary">{status}</Badge>
          ))}
        </FilterBarActiveFilters>
      ) : null}
    </FilterBar>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/filter-bar

Import

import {
  FilterBar,
  FilterBarActions,
  FilterBarActiveFilters,
  FilterBarControls,
  FilterBarFilters,
  FilterBarSearch,
} from "@/components/patterns/filter-bar";

Need the machine-readable source metadata? View registry JSON.

Usage

FilterBar organizes search, filters, active filter badges and actions without owning product state.

import { useState } from "react";
import {
  FilterBar,
  FilterBarActions,
  FilterBarActiveFilters,
  FilterBarControls,
  FilterBarFilters,
  FilterBarSearch,
} from "@/components/patterns/filter-bar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Menu,
  MenuCheckboxItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/ui/menu";
import { SearchIcon, SlidersHorizontalIcon, XIcon } from "lucide-react";

const statusOptions = ["Active", "Trial"] as const;

export function Example() {
  const [query, setQuery] = useState("");
  const [activeStatuses, setActiveStatuses] = useState<Set<string>>(
    () => new Set(["Active"]),
  );
  const hasActiveFilters = Boolean(query || activeStatuses.size);

  function setStatus(status: string, checked: boolean) {
    setActiveStatuses((current) => {
      const next = new Set(current);

      if (checked) {
        next.add(status);
      } else {
        next.delete(status);
      }

      return next;
    });
  }

  function clearFilters() {
    setQuery("");
    setActiveStatuses(new Set());
  }

  return (
    <FilterBar className="w-full max-w-2xl">
      <FilterBarControls>
        <FilterBarSearch>
          <div className="relative">
            <SearchIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
            <Input
              aria-label="Search customers"
              className="pl-8"
              onChange={(event) => setQuery(event.target.value)}
              placeholder="Search customers"
              type="search"
              value={query}
            />
          </div>
        </FilterBarSearch>
        <FilterBarFilters>
          <Menu>
            <MenuTrigger render={<Button type="button" variant="outline" />}>
              <SlidersHorizontalIcon />
              Status
            </MenuTrigger>
            <MenuPopup align="end">
              {statusOptions.map((status) => (
                <MenuCheckboxItem
                  checked={activeStatuses.has(status)}
                  key={status}
                  onCheckedChange={(checked) => setStatus(status, checked)}
                >
                  {status}
                </MenuCheckboxItem>
              ))}
            </MenuPopup>
          </Menu>
          {hasActiveFilters ? (
            <FilterBarActions>
              <Button onClick={clearFilters} type="button" variant="ghost">
                <XIcon />
                Clear
              </Button>
            </FilterBarActions>
          ) : null}
        </FilterBarFilters>
      </FilterBarControls>
      {hasActiveFilters ? (
        <FilterBarActiveFilters>
          {query ? <Badge variant="outline">Search: {query}</Badge> : null}
          {Array.from(activeStatuses).map((status) => (
            <Badge key={status} variant="secondary">{status}</Badge>
          ))}
        </FilterBarActiveFilters>
      ) : null}
    </FilterBar>
  );
}

Purpose

Filter Bar is a registry:block for list and table screens. It organizes search, filter controls, active-filter badges and clear actions without owning product state or query serialization.

API Reference

  • FilterBar provides layout slots only; the consuming screen owns search state, filters and URL synchronization.
  • Use Input for search, Menu or an appropriate selection primitive for filters, Badge for active filters and Button for actions.
  • Show clear actions only when search or filters are active, and keep active filter badges in their own row.
  • Search controls need visible or screen-reader labels. Active filters must describe their state in text rather than color alone.
  • Use density="compact" for a denser bar. Without a local value, density inherits from SiteplaneUIProvider.

Motion

  • The bar itself does not animate. Menus, Selects and other floating controls retain their native floating-panel motion.
  • Reduced motion is inherited from those primitives and the global motion provider.

Accessibility

  • Give every search and filter control a label; a placeholder is supporting copy, not an accessible name.
  • Describe active filters with readable text rather than color alone.
  • Keep clear actions keyboard reachable and name whether they clear one filter or the complete filter state.

Implementation Guidance

  • Keep product filtering logic outside the pattern.
  • Use native Siteplane controls in every slot instead of raw form or menu elements.
  • Prefer the density prop over local spacing overrides.

Registry

Source Contract

Registry item: filter-bar

Files

  • src/components/patterns/filter-bar.tsx to components/patterns/filter-bar.tsx

Registry dependencies: @siteplane/base, @siteplane/badge, @siteplane/button, @siteplane/input, @siteplane/menu

On This Page