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.
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/page-search-filter.jsonUsage
Import from the block file and compose. Both inputs can be controlled or uncontrolled.
TSImport
import { PageSearchFilter } from "@/components/blocks/page-search-filter";TSExample
"use client";
import * as React from "react";
import { Globe, Palette, Tag, UsersRound, Video } from "lucide-react";
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 },
]}
/>
);
}Composition
Anatomy of the PageSearchFilter block.
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-labelDefault
Search input plus pill row. Two logical groups (kits/views vs. categories) split by a hairline divider via `dividerAfter`. Active pill renders in primary fill; inactive pills carry a thin border and muted text.
Multi-select
Pass `multiple` to make pills _after_ the `dividerAfter` boundary multi-selectable while keeping pre-divider pills (kits/views) single-select. Use `activeTab` / `onTabChange` for the single-select group and `activeTabs` / `onTabsChange` for the multi-select set. A `Clear` Button appears inline whenever the multi set is non-empty.
API Reference
Props on PageSearchFilter and the tab item shape.
PageSearchFilter
| 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 lucide-react component works. |