FilterBar
Controlled filter controls with per-item single- and multi-select modes.
"use client"; import { useState } from "react";import { FilterBar, formatConditionLabel, type FilterBarItem } from "@cardspark/ui/core"; const conditionOptions = [ { value: "All Conditions", label: "All Conditions", compactLabel: "All" }, ...["Near Mint", "Lightly Played", "Moderately Played", "Damaged"].map((condition) => ({ value: condition, label: formatConditionLabel(condition, "code-label"), compactLabel: formatConditionLabel(condition, "code") }))];const filterOptions = [ { id: "source", label: "Source", multiple: true, values: ["All Sources"], options: ["All Sources", "TCGplayer", "eBay", "Shopify"] }, { id: "grader", label: "Grader", value: "Ungraded", options: ["Ungraded", "PSA", "CGC", "BGS", "TAG"] }, { id: "condition", label: "Condition", multiple: true, values: ["Lightly Played"], options: conditionOptions, compact: true }, { id: "variant", label: "Variant", multiple: true, values: ["Holofoil", "Reverse Holofoil"], options: ["All Variants", "Normal", "Holofoil", "Reverse Holofoil", "First Edition"] }, { id: "rarity", label: "Rarity", multiple: true, values: ["All Rarities"], options: ["All Rarities", "Rare", "Rare Holo", "Illustration Rare", "Special Illustration Rare", "Hyper Rare"] }] satisfies FilterBarItem[];const sortOptions = ["Most valuable", "Least valuable", "Newest", "Oldest"]; export function FilterBarDemo() { const [filters, setFilters] = useState(filterOptions); const [sort, setSort] = useState(sortOptions[0]); return ( <FilterBar filters={filters} actions={ <label className="cs-filter-control" htmlFor="card-sort"> <span className="cs-filter-label">Sort</span> <span className="cs-filter-select"> <span className="cs-filter-size-probe" aria-hidden="true"> {sort} </span> <select id="card-sort" name="sort" value={sort} aria-label="Sort" onChange={(event) => setSort(event.target.value)}> {sortOptions.map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> <span className="cs-filter-caret" aria-hidden="true">▼</span> </span> </label> } onChange={(id, selection) => { setFilters((current) => current.map((filter) => { if (filter.id !== id) return filter; return filter.multiple ? { ...filter, values: Array.isArray(selection) ? selection : [selection] } : { ...filter, value: Array.isArray(selection) ? selection[0] ?? filter.value : selection }; })); }} /> );}Installation
Install the package.
npm install @cardspark/uiImport the shared stylesheet layer near your app root.
app/layout.tsx
import "@cardspark/ui/tokens.css";import "@cardspark/ui/themes/basement.css";import "@cardspark/ui/styles.css";Import and render FilterBar from its package entry point.
Usage
FilterBarDemo.tsx
"use client"; import { useState } from "react";import { FilterBar, formatConditionLabel, type FilterBarItem } from "@cardspark/ui/core"; const conditionOptions = [ { value: "All Conditions", label: "All Conditions", compactLabel: "All" }, ...["Near Mint", "Lightly Played", "Moderately Played", "Damaged"].map((condition) => ({ value: condition, label: formatConditionLabel(condition, "code-label"), compactLabel: formatConditionLabel(condition, "code") }))];const filterOptions = [ { id: "source", label: "Source", multiple: true, values: ["All Sources"], options: ["All Sources", "TCGplayer", "eBay", "Shopify"] }, { id: "grader", label: "Grader", value: "Ungraded", options: ["Ungraded", "PSA", "CGC", "BGS", "TAG"] }, { id: "condition", label: "Condition", multiple: true, values: ["Lightly Played"], options: conditionOptions, compact: true }, { id: "variant", label: "Variant", multiple: true, values: ["Holofoil", "Reverse Holofoil"], options: ["All Variants", "Normal", "Holofoil", "Reverse Holofoil", "First Edition"] }, { id: "rarity", label: "Rarity", multiple: true, values: ["All Rarities"], options: ["All Rarities", "Rare", "Rare Holo", "Illustration Rare", "Special Illustration Rare", "Hyper Rare"] }] satisfies FilterBarItem[];const sortOptions = ["Most valuable", "Least valuable", "Newest", "Oldest"]; export function FilterBarDemo() { const [filters, setFilters] = useState(filterOptions); const [sort, setSort] = useState(sortOptions[0]); return ( <FilterBar filters={filters} actions={ <label className="cs-filter-control" htmlFor="card-sort"> <span className="cs-filter-label">Sort</span> <span className="cs-filter-select"> <span className="cs-filter-size-probe" aria-hidden="true"> {sort} </span> <select id="card-sort" name="sort" value={sort} aria-label="Sort" onChange={(event) => setSort(event.target.value)}> {sortOptions.map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> <span className="cs-filter-caret" aria-hidden="true">▼</span> </span> </label> } onChange={(id, selection) => { setFilters((current) => current.map((filter) => { if (filter.id !== id) return filter; return filter.multiple ? { ...filter, values: Array.isArray(selection) ? selection : [selection] } : { ...filter, value: Array.isArray(selection) ? selection[0] ?? filter.value : selection }; })); }} /> );}Composition
Use FilterBar as the control strip around a data-owning surface; keep the actual filtering and sorting state in the parent.
FilterableSurface├── Header / result count├── FilterBar│ ├── filter definitions│ ├── selected values│ └── optional actions└── Results owned by the parent surfaceFilterBar reports changes by filter id, while the surrounding surface filters data, handles loading states, and renders the resulting content.
Features
- Renders
filtersas compact select and multi-select controls with shared package spacing and overflow behavior. - Uses each item's
multiplediscriminator to pairvaluewith single-select items andvalueswith multi-select items. - Reports both selection shapes through one
onChangecallback and forwards native section props and refs.
Examples
Multiselect states
Show all-value, single-value, multi-value, compact-label, and disabled-option multiselect states in the same control pattern.
"use client"; import { useState } from "react";import { FilterBar, formatConditionLabel, type FilterBarItem } from "@cardspark/ui/core"; const multiselectStateExamples: Array<{ title: string; filters: FilterBarItem[] }> = [ { title: "All values", filters: [ { id: "source", label: "Source", multiple: true, values: ["All Sources"], options: ["All Sources", "TCGplayer", "eBay", "Shopify"] } ] }, { title: "Single value", filters: [ { id: "rarity", label: "Rarity", multiple: true, values: ["Rare Holo"], options: ["All Rarities", "Common", "Uncommon", "Rare", "Rare Holo", "Illustration Rare"] } ] }, { title: "Multiple values", filters: [ { id: "variant", label: "Variant", multiple: true, values: ["Holofoil", "Reverse Holofoil"], options: ["All Variants", "Normal", "Holofoil", "Reverse Holofoil", "First Edition"] } ] }, { title: "Compact labels", filters: [ { id: "condition", label: "Condition", multiple: true, values: ["Lightly Played"], options: [ { value: "All Conditions", label: "All Conditions", compactLabel: "All" }, ...["Near Mint", "Lightly Played", "Moderately Played", "Damaged"].map((condition) => ({ value: condition, label: formatConditionLabel(condition, "code-label"), compactLabel: formatConditionLabel(condition, "code") })) ], compact: true } ] }, { title: "Unavailable options", filters: [ { id: "availability", label: "Availability", multiple: true, values: ["In Stock"], options: ["All Availability", "In Stock", { value: "Preorder", disabled: true }, "Sold"] } ] }]; export function FilterBarMultiselectStates() { const [exampleFilters, setExampleFilters] = useState(multiselectStateExamples.map((example) => example.filters)); function updateExampleFilter(exampleIndex, id, values) { setExampleFilters((currentExamples) => currentExamples.map((filters, currentIndex) => currentIndex === exampleIndex ? filters.map((filter) => filter.id === id && filter.multiple ? { ...filter, values } : filter) : filters ) ); } return ( <div className="filter-bar-state-examples"> {multiselectStateExamples.map((example, index) => ( <section key={example.title}> <h3>{example.title}</h3> <FilterBar filters={exampleFilters[index]} onChange={(id, selection) => updateExampleFilter( index, id, Array.isArray(selection) ? selection : [selection] )} /> </section> ))} </div> );}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
filters | FilterBarItem[] | - | Controlled filters. Single items use `value`; items with `multiple: true` use `values`. |
actions | ReactNode | - | Optional trailing action content rendered inside the component. |
onChange | (id: string, selection: string | string[]) => void | - | Reports a controlled filter selection using the item's single or multi-select shape. |
ariaLabel | string | "Filters" | Accessible label for the filter region. |