Category Chip
A small pill-shaped button that represents a category, topic, or label. Selection state is controlled by the call-site — the chip itself is stateless. Used standalone in post metadata, list rows, or composed into a CategoryFilterBar for a horizontal filter row.
Customize
State
0
Tone
default
Installation
npx shadcn@latest add https://craftbits.dev/r/category-chip.jsonUsage
import { CategoryChip } from "@craft-bits/core";
<CategoryChip selected={category === "rust"} onClick={() => setCategory("rust")}>
Rust
</CategoryChip>Pass a count to surface the number of items in the category:
<CategoryChip count={42}>Systems</CategoryChip>Switch the unselected emphasis to accent for a tone that previews the selected state:
<CategoryChip tone="accent">Featured</CategoryChip>Understanding the component
- Stateless surface. The chip owns no selection state —
selectedis a prop,onClickis a prop. Callers manage the source of truth (URL, local state, store). This keeps the chip composable insideCategoryFilterBar, lists, and bespoke filter UIs without prop bleed. - CVA-driven states. Selected / unselected styling lives in one
categoryChipVariantsblock. Selected chips paint the accent surface with anaccent-fglabel; unselected chips show an outlined transparent surface withfg-mutedtext and a subtle hover lift. Thetone="accent"variant only modifies the unselected look — selected always wins. - Motion polish.
whileTapuses the canonicalTAP_SCALE(0.96), driven bySPRINGS.snapfor a crisp press-release feel. Color transitions run at 150ms — well under the 300ms ceiling. Disabled chips skip the tap animation. - A11y baked in. Renders a
<button>witharia-pressedreflecting theselectedboolean, so screen readers announce "Rust, toggle button, pressed" without any extra wiring. Adata-state="selected"attribute hooks CSS-driven styles for consumers that prefer state attributes. - Optional count. When
count >= 0, a dimmed mono pill renders after the label. It'saria-hiddenbecause the surrounding label already conveys what the chip filters — the count is supplementary.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | Drives the filled accent surface and aria-pressed. |
disabled | boolean | false | Inert state — keeps the chip in the DOM, removes the tap response. |
count | number | — | Optional count, rendered in a dimmed mono pill after the label. |
tone | 'default' | 'accent' | 'default' | Unselected emphasis. Selected always uses the accent surface regardless of tone. |
children | ReactNode | — | Label content. |
className | string | — | Merged onto the rendered <button>. |
...rest | HTMLMotionProps<'button'> | — | Any other Motion-aware button prop. |
Accessibility
- The chip is a
<button>witharia-pressedreflecting theselectedprop — screen readers announce "pressed" / "not pressed" state. disabledis the native HTML attribute — keyboard and pointer interactions are blocked, and the chip leaves the tab order.- A
focus-visible:ring keyed to--cb-accent, offset from--cb-bg, ensures keyboard users can always see the focused chip on every theme surface. - Color contrast in the default theme: unselected label uses
--cb-fg-mutedagainst--cb-bg; selected uses--cb-accent-fgagainst--cb-accent. Both pass WCAG AA.
Credits
- Extracted from:
terminal-dreams(src/components/principles/CategoryChip.tsx). The original baked innext/linkrouting, per-category color tokens, and a hardcodedCATEGORIESlookup. craft-bits lifts the chip into a button primitive: the consumer drives selection, the call-site decides routing, and styling consumes our accent token so it themes.