UI / Core

FilterBar

Controlled filter controls with per-item single- and multi-select modes.

Source
All Sources
Condition
LP
Variant
2 Variants
Rarity
All Rarities
"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/ui

Import 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.

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 };        }));      }}    />  );}

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 surface

FilterBar reports changes by filter id, while the surrounding surface filters data, handles loading states, and renders the resulting content.

Features

  • Renders filters as compact select and multi-select controls with shared package spacing and overflow behavior.
  • Uses each item's multiple discriminator to pair value with single-select items and values with multi-select items.
  • Reports both selection shapes through one onChange callback 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.

All values

Selecting the all option returns the compact add-filter affordance.

Source
All Sources

Single value

One selected value renders the selected option label directly in the trigger.

Rarity
Rare Holo

Multiple values

Multiple active values collapse to a count and expose clear behavior from the menu.

Variant
2 Variants

Compact labels

Compact options keep dense trading-card state readable in narrow rows.

Condition
LP

Unavailable options

Disabled options remain visible in the menu while staying unavailable for selection.

Availability
In Stock
"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

PropTypeDefaultDescription
filtersFilterBarItem[]-Controlled filters. Single items use `value`; items with `multiple: true` use `values`.
actionsReactNode-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.
ariaLabelstring"Filters"Accessible label for the filter region.