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

Usage

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

  1. Pure layout. CategoryGrid only owns the wrapping <div>'s grid-template-columns and gap. Each item spreads onto a child CategoryCard, so every prop on CategoryCard is reachable through the items array (variant, size, href, icon, aria-label, onClick, …).
  2. 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. Set minColumnWidth to control the intrinsic column floor (defaults to 18rem, which fits the default md card size).
  3. 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.
  4. Stagger is opt-in and bounded. staggered={true} fades + lifts cards in with a per-sibling delay of STAGGER (0.04s) — comfortably under the craft-bits/no-excessive-stagger cap of 0.05s. useReducedMotion() short-circuits the animation entirely so the grid renders instantly when the user prefers reduced motion.
  5. id per item. Items must carry a stable id (or key). Indexes are never used as a fallback when an id is 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

PropTypeDefaultDescription
itemsCategoryGridItem[]requiredCards to render. Each entry is a CategoryCardProps plus a stable id (or key).
minColumnWidthstring'18rem'Floor for each auto-fill track. Ignored when columns is set.
columnsnumberFixed track count. Overrides minColumnWidth.
gapnumber | string3Numbers map to Tailwind spacing units (3 = 0.75rem); strings are used verbatim.
staggeredbooleanfalseFades + lifts cards in on first mount. Respects prefers-reduced-motion.
itemRolestringARIA role applied to each card wrapper. Pair with role="list" on the root for semantic lists.
classNamestringMerged 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), pair role="list" on the root with itemRole="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 off title, 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 items to reorder tab focus.

Credits

  • Extracted from: terminal-dreams (src/components/cookbook/CategoryGrid.tsx). The source coupled the grid to project-specific CategoryInfo / CookbookRecipe types, the getCategoryIllustration helper, 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 flat items: CategoryCardProps[] array, exposes minColumnWidth / columns / gap for layout control, replaces the hardcoded breakpoint grid with CSS Grid's auto-fill + minmax pattern, and drops the embedded recipe expander entirely — pair CategoryGrid with any disclosure component when an expandable detail panel is needed.