Category Grid
A responsive grid wrapper for a list of CategoryCards. Owns sizing, spacing, and an optional staggered entrance — every per-card concern (variant, size, href, icon, …) is passed through the items array.
Customize
Items
6
Layout
18rem
3
Motion
Installation
npx shadcn@latest add https://craftbits.dev/r/category-grid.jsonUsage
Pass an array of CategoryCardProps plus a stable id per item:
import { CategoryGrid } from "@craft-bits/core";
<CategoryGrid
items={[
{
id: "streaming",
href: "/cookbook/streaming-ui",
title: "Streaming UI",
description: "Patterns for incremental rendering and Server-Sent Events.",
meta: "12 recipes",
},
{
id: "perf",
href: "/cookbook/perf",
title: "Performance",
description: "Hydration, bundle splitting, and render-cost budgets.",
meta: "9 recipes",
},
]}
/>With a staggered entrance:
<CategoryGrid items={items} staggered />With a fixed three-column layout:
<CategoryGrid items={items} columns={3} gap="1rem" />Understanding the component
- Pure layout.
CategoryGridonly owns the wrapping<div>'sgrid-template-columnsandgap. Each item spreads onto a childCategoryCard, so every prop onCategoryCardis reachable through theitemsarray (variant,size,href,icon,aria-label,onClick, …). - Responsive without media queries. The grid uses CSS Grid's
repeat(auto-fill, minmax(<min>, 1fr))pattern. As the viewport shrinks, tracks collapse and cards reflow — no breakpoints needed. SetminColumnWidthto control the intrinsic column floor (defaults to18rem, which fits the defaultmdcard size). - Fixed columns when you need them. Pass
columns={3}to lock a track count regardless of viewport width. The minimum-column rule is ignored in that mode. - Stagger is opt-in and bounded.
staggered={true}fades + lifts cards in with a per-sibling delay ofSTAGGER(0.04s) — comfortably under thecraft-bits/no-excessive-staggercap of 0.05s.useReducedMotion()short-circuits the animation entirely so the grid renders instantly when the user prefers reduced motion. idper item. Items must carry a stableid(orkey). Indexes are never used as a fallback when anidis present — that's what keeps stagger and any future layout animations stable as items reorder.
Variants
Three columns, generous gap:
<CategoryGrid items={items} columns={3} gap="1.25rem" />Two columns on a narrow surface (sidebar / drawer):
<CategoryGrid items={items} minColumnWidth="14rem" gap={2} />Staggered entrance on a landing page:
<CategoryGrid items={items} staggered minColumnWidth="20rem" />Semantic list (announce to screen readers as a list of categories):
<CategoryGrid
items={items}
role="list"
itemRole="listitem"
aria-label="Cookbook categories"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | CategoryGridItem[] | required | Cards to render. Each entry is a CategoryCardProps plus a stable id (or key). |
minColumnWidth | string | '18rem' | Floor for each auto-fill track. Ignored when columns is set. |
columns | number | — | Fixed track count. Overrides minColumnWidth. |
gap | number | string | 3 | Numbers map to Tailwind spacing units (3 = 0.75rem); strings are used verbatim. |
staggered | boolean | false | Fades + lifts cards in on first mount. Respects prefers-reduced-motion. |
itemRole | string | — | ARIA role applied to each card wrapper. Pair with role="list" on the root for semantic lists. |
className | string | — | Merged onto the grid root via cn(). |
Accessibility
- The grid is a plain
<div>by default. When the items are semantic (e.g. a category index), pairrole="list"on the root withitemRole="listitem"so assistive tech announces the relationship. - Per-card a11y is the responsibility of
CategoryCard— every card is a real<a>or<button>, every card has an accessible name keyed offtitle, every card is keyboard-activatable. prefers-reduced-motion: the optional entrance stagger collapses to no animation when the user requests reduced motion. The CSS Grid layout itself never animates, so no further reduced-motion fallback is needed.- Focus order follows DOM order. Items render in array order, so reorder
itemsto reorder tab focus.
Credits
- Extracted from:
terminal-dreams(src/components/cookbook/CategoryGrid.tsx). The source coupled the grid to project-specificCategoryInfo/CookbookRecipetypes, thegetCategoryIllustrationhelper, and an embedded expand-on-click recipe panel keyed on the active category. craft-bits keeps the responsive layout but reshapes the API to a flatitems: CategoryCardProps[]array, exposesminColumnWidth/columns/gapfor layout control, replaces the hardcoded breakpoint grid with CSS Grid'sauto-fill+minmaxpattern, and drops the embedded recipe expander entirely — pairCategoryGridwith any disclosure component when an expandable detail panel is needed.