A search input paired with a horizontal pill filter strip for gating long lists by a free-text query and either a one-of-many or many-of-many category set.
npx shadcn@latest add https://sdk-components.thesqd.com/r/page-search-filter.jsonimport { PageSearchFilter } from "@/components/blocks/page-search-filter";"use client";
import * as React from "react";
import { Globe, Palette, Tag, Users as UsersRound, VideoCamera as Video } from "@phosphor-icons/react/ssr";
import { PageSearchFilter } from "@/components/blocks/page-search-filter";
export function Example() {
const [search, setSearch] = React.useState("");
const [tab, setTab] = React.useState("all");
return (
<PageSearchFilter
searchPlaceholder="Search projects..."
searchValue={search}
onSearchChange={setSearch}
activeTab={tab}
onTabChange={setTab}
dividerAfter="all"
tabs={[
{ key: "squadkits", label: "SquadKits" },
{ key: "my-kits", label: "My Kits" },
{ key: "most-used", label: "My Most Used" },
{ key: "all", label: "All Projects" },
{ key: "design", label: "Design", icon: Palette },
{ key: "video", label: "Video", icon: Video },
{ key: "social", label: "Social", icon: UsersRound },
{ key: "web", label: "Web", icon: Globe },
{ key: "brand", label: "Brand", icon: Tag },
]}
/>
);
}PageSearchFilter (single block — pass everything as props)
├── searchValue / defaultSearchValue / onSearchChange (controlled or uncontrolled)
├── searchPlaceholder (default "Search…")
├── tabs={[{ key, label, icon? }]} (lucide component for icon)
├── activeTab / defaultActiveTab / onTabChange (single-select — controlled or uncontrolled)
├── multiple (post-divider pills become multi-select; pre-divider stays single-select)
├── activeTabs / defaultActiveTabs / onTabsChange (multi-select — controlled or uncontrolled)
├── dividerAfter="<key>" (inserts a hairline between groups)
├── className (root override)
└── aria-labelPageSearchFilter
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | PageSearchFilterTab[] | — | Pill filter items rendered in order. |
activeTab | string | — | Controlled active tab key (single-select mode). |
defaultActiveTab | string | tabs[0].key | Uncontrolled initial active key (single-select mode). |
onTabChange | (key: string) => void | — | Fires when a pill is clicked in single-select mode. |
multiple | boolean | false | Make pills _after_ `dividerAfter` multi-select toggles. Pre-divider pills remain single-select. Use with `activeTabs` / `onTabsChange`. |
activeTabs | string[] | — | Controlled selected tab keys (multi-select mode). |
defaultActiveTabs | string[] | [] | Uncontrolled initial selected keys (multi-select mode). |
onTabsChange | (keys: string[]) => void | — | Fires when a pill is toggled in multi-select mode. |
searchValue | string | — | Controlled search text. |
defaultSearchValue | string | "" | Uncontrolled initial search text. |
onSearchChange | (value: string) => void | — | Fires on every keystroke in the search input. |
searchPlaceholder | string | "Search…" | Placeholder on the search input. |
dividerAfter | string | — | Tab `key` after which to insert a vertical hairline divider — used to separate kit/filter groups from category groups. |
className | string | — | Override the root container layout. |
aria-label | string | — | Accessible label for the filter region. |
PageSearchFilterTab
| Prop | Type | Default | Description |
|---|---|---|---|
key | string | — | Stable identifier — used for the controlled state and `dividerAfter`. |
label | ReactNode | — | Visible pill text. |
icon | ComponentType<{ className?: string }> | — | Optional leading icon. Any Phosphor component works. |