Recipe Card

A media-style card for "cookbook" recipes — a step-by-step technical guide. Renders a 16:9 preview image at the top, then a body with the serif title, a short description, and a footer row carrying the author, duration, and tags. When no image is supplied, the card paints a colored gradient fallback derived from accentSeed so every recipe still has a stable identity color before anything loads.

Customize
Media
Author
Tags
2

Installation

npx shadcn@latest add https://craftbits.dev/r/recipe-card.json

Usage

import { RecipeCard } from "@craft-bits/core";
 
<RecipeCard
  href="/cookbook/token-streaming-chat-ui"
  title="Building a Token-Streaming Chat UI"
  description="A practical walk through Server-Sent Events, backpressure, and the small rendering tricks that make incremental text feel instant."
  image="/cookbook/streaming-chat.png"
  author={{ name: "Ada Lovelace", avatar: "/avatars/ada.png" }}
  duration="~30 min"
  tags={["streaming", "react"]}
/>

Without an image, pass a stable accentSeed so every visit paints the same color:

<RecipeCard
  href="/cookbook/token-streaming-chat-ui"
  title="Building a Token-Streaming Chat UI"
  accentSeed="token-streaming-chat-ui"
  author={{ name: "Ada Lovelace" }}
  duration="~30 min"
  tags={["streaming", "react"]}
/>

Bring-your-own media (e.g. next/image):

import Image from "next/image";
 
<RecipeCard
  href="/cookbook/token-streaming-chat-ui"
  title="Building a Token-Streaming Chat UI"
  image={<Image src="/cookbook/streaming-chat.png" alt="" fill className="object-cover" />}
/>

Understanding the component

  1. Anchor at the root. The whole card is a single <a> so the entire surface is one click target — no nested-link traps. forwardRef<HTMLAnchorElement> lets Next.js Link (or any router) hand off the underlying anchor.
  2. Media area is 16:9. A fixed aspect ratio means every card in a grid lines up regardless of image dimensions. A string image renders as an <img> with loading="lazy" and a thin token outline; a ReactNode is rendered as-is so callers can drop in next/image or a video.
  3. Gradient fallback paints on every empty card. When image is omitted, the media area is filled by linear-gradient(135deg, hsl(h, 60%, 78%), hsl(h+40, 60%, 65%)) where h is a cheap stable hash of accentSeed (defaulting to the title). A faint serif initial sits on top — enough editorial signal to read the card without a thumbnail. The same seed always produces the same color, so a recipe's identity color is consistent across renders.
  4. Hover lift is two-property only. The transition allow-list is box-shadow,transform,border-color — never transition-all. On hover the card lifts -translate-y-0.5, the two-layer shadow deepens, the border deepens to cb-border-strong, and the title color shifts toward the accent. All scoped properties; no layout thrash.
  5. Shadows never use pure black. Both shadow layers are neutral rgb(15, 15, 15) at low alpha. Enforced by the no-pure-black-shadow lint rule.
  6. Description is clamped to three lines. line-clamp-3 keeps card heights even when descriptions vary.

Props

PropTypeDefaultDescription
titlestringrequiredCard title — serif, large.
descriptionReactNodeShort body excerpt — clamped to 3 lines.
imagestring | ReactNodeA string URL renders as <img>; a ReactNode is rendered as-is. When absent, falls back to a gradient block derived from accentSeed.
accentSeedstringtitleStable seed hashed to a hue 0-360 for the gradient fallback.
author{ name: string; avatar?: string }Author block. When avatar is omitted, an initial circle is rendered.
durationstringDuration label (e.g. '~30 min').
tagsreadonly string[]Small inline tag badges in the footer.
hrefstringrequiredDestination URL.
classNamestringMerged onto the rendered <a> via cn().

Accessibility

  • The card is one anchor — the title, description, author, duration, and tags all read as part of the link's accessible name.
  • Images use empty alt="" because they are decorative previews of the destination; the title (which is part of the same link) already announces what the link is about.
  • Decorative separators (the · between author and duration) are marked aria-hidden="true".
  • The fallback gradient block is aria-hidden="true" — the screen reader never describes it.
  • Focus ring is :focus-visible only — a 2px accent ring with a 2px offset against the page background.
  • The hover lift transitions only box-shadow, transform, and border-color — never transition-all. Animations complete in under 300ms.

Credits

  • Extracted from: terminal-dreams (src/components/cookbook/RecipeCard.tsx). The original used framer-motion for an enter/exit fade-and-rise driven by parent-list state; the library version drops the motion lib in favor of a pure-CSS hover lift, replaces the project-specific CookbookRecipe type with flat props, and adds the accent-seeded gradient fallback so cards without preview images still look intentional.