Filter Chips
Multi-select filter chips where a tick slides in on select, with a live count and a Clear that appears when needed.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/filter-chips.json'use client'
import { useState } from 'react'
import './filter-chips.css'
export interface FilterChipsProps {
options: string[]
value?: string[]
defaultValue?: string[]
onValueChange?: (v: string[]) => void
count?: (v: string[]) => number
layout?: 'wrap' | 'scroll'
label?: string
fill?: 'dark' | 'light'
className?: string
}
/** Multi-select toggle chips with a tick and Clear. */
export function FilterChips({ options, value, defaultValue = [], onValueChange, count, layout = 'wrap', label = 'Filters', fill = 'dark', className }: FilterChipsProps) {
const [inner, setInner] = useState(defaultValue)
const v = value ?? inner
const set = (n: string[]) => { setInner(n); onValueChange?.(n) }
const n = count?.(v)
return (
<div className={['sd-fc', className].filter(Boolean).join(' ')} data-fill={fill} data-wrap={layout}>
<div className="sd-fc__row" role="group" aria-label={label}>
{options.map((o) => {
const on = v.includes(o)
return (
<button key={o} type="button" className="sd-fc__chip" aria-pressed={on} onClick={() => set(on ? v.filter((x) => x !== o) : [...v, o])}>
<span className="sd-fc__tick" aria-hidden><svg viewBox="0 0 16 16"><path d="m3.5 8.5 3 3 6-7" /></svg></span>
{o}
</button>
)
})}
{v.length > 0 && <button type="button" className="sd-fc__clear" onClick={() => set([])}>Clear</button>}
</div>
{n !== undefined && <div className="sd-fc__foot" aria-live="polite"><b>{n}</b> results</div>}
</div>
)
}.sd-fc { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-fc[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-fc button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-fc :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-fc .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-fc { width: min(100%, 330px); }
.sd-fc__row { display: flex; flex-wrap: wrap; gap: 7px; }
.sd-fc[data-wrap='scroll'] .sd-fc__row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.sd-fc__chip { display: inline-flex; align-items: center; height: 34px; padding: 0 14px; border-radius: 999px; font-size: 13.5px; font-weight: 500; box-shadow: inset 0 0 0 1px var(--line); background: var(--bg) !important; transition: background-color 200ms, color 200ms, padding 260ms cubic-bezier(0.3, 1.3, 0.4, 1), transform 140ms; white-space: nowrap; flex: none; }
.sd-fc__chip:hover { box-shadow: inset 0 0 0 1px var(--mute); }
.sd-fc__chip:active { transform: scale(0.95); }
.sd-fc__chip[aria-pressed='true'] { background: var(--strong) !important; color: var(--on) !important; box-shadow: none; padding-left: 10px; }
.sd-fc__tick { display: inline-block; width: 0; overflow: hidden; transition: width 260ms cubic-bezier(0.3, 1.3, 0.4, 1); }
.sd-fc__chip[aria-pressed='true'] .sd-fc__tick { width: 20px; }
.sd-fc__tick svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; }
.sd-fc__clear { color: var(--mute); text-decoration: underline; text-underline-offset: 3px; font-size: 13px; padding: 0 6px; animation: sd-fc-in 220ms ease; }
@keyframes sd-fc-in { from { opacity: 0; transform: translateX(-4px); } }
.sd-fc__foot { margin-top: 14px; font-size: 12.5px; color: var(--mute); }
.sd-fc__foot b { color: var(--fg); font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .sd-fc *, .sd-fc *::before, .sd-fc *::after { animation: none !important; transition: none !important; } }Overview
Selecting a chip grows a tick into it and inverts its colours; the width change is animated so the row reflows smoothly.
A Clear chip fades in once anything is on, and the result count updates as filters change.
When to use
- Filtering lists, galleries and search results by tag.
When not to use
- Single choice: use a segmented control.
- Dozens of options: use a combobox.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/filter-chips.json$
npx shadcn@latest add https://sundaeui.dev/r/filter-chips.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/filter-chips.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/filter-chips.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy filter-chips.tsx and filter-chips.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { FilterChips } from '@/components/sundae/filter-chips'
<FilterChips />Props
| Prop | Type | Default |
|---|---|---|
options*Chip labels. | string[] | — |
value / defaultValueSelected chips. | string[] | — |
onValueChangeCalled on change. | (v: string[]) => void | — |
countOptional result count for the selection. | (v: string[]) => number | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- A group of toggle buttons with aria-pressed.
- Result counts are announced politely.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-fc; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Filter Chips free?
- Yes. Filter Chips is free for personal and commercial projects, with full source on this page.
- How do I install Filter Chips?
- Run npx shadcn@latest add https://sundaeui.dev/r/filter-chips.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Filter Chips support keyboard and screen readers?
- A group of toggle buttons with aria-pressed. Result counts are announced politely.
- Does Filter Chips respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Multi Select
Forms · RefinedTag Field
Forms · UnderstatedSearch Field
Forms · Refined- Pro
Date Range Presets
Forms · Refined Size Picker
Forms · RefinedInvite Members
Forms · Refined