Category Card
A clickable card primitive that introduces a category or topic. Renders as a single click surface — an anchor when href is provided, a button otherwise — with a serif title, an optional description, an optional trailing icon / illustration slot, and a subtle Motion-driven hover lift plus tap-press.
Customize
Variant
surface
Size
md
Slots
Installation
npx shadcn@latest add https://craftbits.dev/r/category-card.jsonUsage
As a link (the common case):
import { CategoryCard } from "@craft-bits/core";
<CategoryCard
href="/cookbook/streaming-ui"
title="Streaming UI"
description="Patterns for incremental rendering, backpressure, and Server-Sent Events."
meta="12 recipes"
/>As a button (no navigation — e.g. expand an inline panel):
<CategoryCard
title="Streaming UI"
description="Patterns for incremental rendering and Server-Sent Events."
meta="12 recipes"
onClick={() => setExpanded(true)}
/>With an inline SVG icon slot:
<CategoryCard
href="/cookbook/streaming-ui"
title="Streaming UI"
description="Patterns for incremental rendering and Server-Sent Events."
icon={<MyCategoryIllustration />}
/>Understanding the component
- Polymorphic root. When
hrefis provided the card renders as amotion.a; otherwise it renders as amotion.buttonwithtype="button".forwardReftargets the unionHTMLAnchorElement | HTMLButtonElementso callers can hand off the underlying element to Next.js / Remix routers without writing a wrapper. - Single click target. The entire surface is one element — the title, description, meta, and icon all read as part of the same accessible name. The
aria-labeldefaults to the visibletitlewhen it is a string and can be overridden when callers pass aReactNode. - Hover lift is a transform. On hover the card translates
y: -2and the two-layer non-pure-black shadow deepens, the border deepens tocb-border-strong, and the title color shifts towardcb-accent. The transition allow-list isbackground-color,border-color,box-shadow,color— nevertransition-all. - Tap-press uses TAP_SCALE. The shared
TAP_SCALEtoken is applied onwhileTap— the same press feel every other interactive primitive in the library uses. BothwhileHoverandwhileTapshort-circuit toundefinedwhenuseReducedMotion()reports the user prefers reduced motion. - Three visual variants.
surface(default) is the bordered elevated card with the soft shadow.outlineis a transparent bordered surface, useful inside dense grids where the shadow would be noise.ghostis borderless and paints a tinted hover background, useful in vertical lists where the card sits flush with the page.
Variants
<CategoryCard variant="surface" title="Streaming UI" />
<CategoryCard variant="outline" title="Streaming UI" />
<CategoryCard variant="ghost" title="Streaming UI" />Sizes:
<CategoryCard size="sm" title="Streaming UI" />
<CategoryCard size="md" title="Streaming UI" />
<CategoryCard size="lg" title="Streaming UI" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | required | Card title — serif, prominent. |
description | ReactNode | — | Optional supporting line below the title. |
meta | ReactNode | — | Optional small line below the description (e.g. '12 recipes'). |
icon | ReactNode | — | Optional icon / illustration rendered to the right of the body. |
href | string | — | When present the root renders as <a>; otherwise it renders as <button type="button">. |
variant | 'surface' | 'outline' | 'ghost' | 'surface' | Visual style of the card. |
size | 'sm' | 'md' | 'lg' | 'md' | Inner padding and gap. |
aria-label | string | title (when string) | Override the accessible name. Required when title is a non-string ReactNode. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- Renders a real
<a>or<button type="button">— keyboard activation works natively. aria-labeldefaults to the visible stringtitle; pass an explicitaria-labelwhentitleis a non-stringReactNode.- The icon slot is
aria-hidden="true"— the title already announces the card's intent. - Focus ring is
:focus-visibleonly — a 2px accent ring with a 2px offset against the page background. prefers-reduced-motion: bothwhileHoverandwhileTapcollapse toundefined, so the card stays still for users who request reduced motion.- Color contrast: the title uses
--cb-fgon--cb-bg-elevated(surface) or--cb-bg(outline / ghost); both pass WCAG AA in the default theme.
Credits
- Extracted from:
terminal-dreams(src/components/cookbook/CategoryCard.tsx). The source wrapped a recipe-grid expander tied to a project-specificCategoryInfo/CookbookRecipetype and a project-specificgetCategoryIllustrationhelper. craft-bits keeps the visual identity but reshapes the API to a generic clickable card: polymorphic anchor / button root, flattitle/description/icon/metaprops,surface/outline/ghostvariants,sm/md/lgsizes,SPRINGS.snap+TAP_SCALEmotion,cb-*semantic tokens, and a reduced-motion fallback. The cookbook-specific expand / collapse panel and the embeddedRecipeCardgrid are intentionally dropped — pairCategoryCardwith any disclosure component when a grid is needed.