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.jsonUsage
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
- 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.jsLink(or any router) hand off the underlying anchor. - Media area is 16:9. A fixed aspect ratio means every card in a grid lines up regardless of image dimensions. A string
imagerenders as an<img>withloading="lazy"and a thin token outline; aReactNodeis rendered as-is so callers can drop innext/imageor a video. - Gradient fallback paints on every empty card. When
imageis omitted, the media area is filled bylinear-gradient(135deg, hsl(h, 60%, 78%), hsl(h+40, 60%, 65%))wherehis a cheap stable hash ofaccentSeed(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. - Hover lift is two-property only. The transition allow-list is
box-shadow,transform,border-color— nevertransition-all. On hover the card lifts-translate-y-0.5, the two-layer shadow deepens, the border deepens tocb-border-strong, and the title color shifts toward the accent. All scoped properties; no layout thrash. - Shadows never use pure black. Both shadow layers are neutral
rgb(15, 15, 15)at low alpha. Enforced by theno-pure-black-shadowlint rule. - Description is clamped to three lines.
line-clamp-3keeps card heights even when descriptions vary.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Card title — serif, large. |
description | ReactNode | — | Short body excerpt — clamped to 3 lines. |
image | string | ReactNode | — | A string URL renders as <img>; a ReactNode is rendered as-is. When absent, falls back to a gradient block derived from accentSeed. |
accentSeed | string | title | Stable 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. |
duration | string | — | Duration label (e.g. '~30 min'). |
tags | readonly string[] | — | Small inline tag badges in the footer. |
href | string | required | Destination URL. |
className | string | — | Merged 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 markedaria-hidden="true". - The fallback gradient block is
aria-hidden="true"— the screen reader never describes it. - Focus ring is
:focus-visibleonly — a 2px accent ring with a 2px offset against the page background. - The hover lift transitions only
box-shadow,transform, andborder-color— nevertransition-all. Animations complete in under 300ms.
Credits
- Extracted from:
terminal-dreams(src/components/cookbook/RecipeCard.tsx). The original usedframer-motionfor 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-specificCookbookRecipetype with flat props, and adds the accent-seeded gradient fallback so cards without preview images still look intentional.