Page search/filter

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.json
Usage
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-label
Default
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

PropTypeDefaultDescription
tabsPageSearchFilterTab[]Pill filter items rendered in order.
activeTabstringControlled active tab key (single-select mode).
defaultActiveTabstringtabs[0].keyUncontrolled initial active key (single-select mode).
onTabChange(key: string) => voidFires when a pill is clicked in single-select mode.
multiplebooleanfalseMake pills _after_ `dividerAfter` multi-select toggles. Pre-divider pills remain single-select. Use with `activeTabs` / `onTabsChange`.
activeTabsstring[]Controlled selected tab keys (multi-select mode).
defaultActiveTabsstring[][]Uncontrolled initial selected keys (multi-select mode).
onTabsChange(keys: string[]) => voidFires when a pill is toggled in multi-select mode.
searchValuestringControlled search text.
defaultSearchValuestring""Uncontrolled initial search text.
onSearchChange(value: string) => voidFires on every keystroke in the search input.
searchPlaceholderstring"Search…"Placeholder on the search input.
dividerAfterstringTab `key` after which to insert a vertical hairline divider — used to separate kit/filter groups from category groups.
classNamestringOverride the root container layout.
aria-labelstringAccessible label for the filter region.

PageSearchFilterTab

PropTypeDefaultDescription
keystringStable identifier — used for the controlled state and `dividerAfter`.
labelReactNodeVisible pill text.
iconComponentType<{ className?: string }>Optional leading icon. Any lucide-react component works.