Next.js Card — Server-Rendered Card Grids
Render Card grids in Next.js 16 server components with the Drivn Card — no 'use client', wrap whole cards in next/link, zero runtime UI dependencies.
Most card components drag a client boundary behind them — a hover hook here, a click handler there — and before long a page that should be static is shipping JavaScript it never needed. The Drivn Card avoids that entirely. Its source carries no 'use client' directive, no useState, and no event handlers: CardRoot is a plain <div> that merges a styles.base class with an optional hover class through the local cn helper. That makes it a server component by default, which is exactly what you want for the most common card use case in Next.js — a grid of content fetched on the server and rendered once.
Because the file has zero runtime dependencies (no Radix, no cva, not even an icon import), a grid of cards rendered inside an async server component ships no client bundle for the cards at all. The hover lift is pure CSS, the layout is pure Tailwind, and the whole card can be wrapped in a next/link for navigation without crossing the server-client divide.
This guide installs the Card in a Next.js 16 App Router project, renders a server-component card grid from fetched data, makes each card a prefetching link, composes the Card.Preview and Card.Info slots, and customizes the styles object. Every snippet matches the source the CLI writes. For the full slot reference see the Card docs; for the comparison see Drivn vs shadcn/ui Card.
Install the Card in a Next.js 16 project
Drivn has no runtime package to install — the CLI copies the component source into your repository and leaves. From the root of your Next.js 16 project run npx drivn add card. The CLI asks once where components live, defaulting to src/components/ui/, and writes card.tsx there. Open the file and the dependency story is the shortest of any Drivn component: it imports React and the local cn class-merge helper, and nothing else — no lucide-react, no third-party UI primitive, no hook. Everything visual lives in a styles object at the top of the file. The CLI reference covers custom directories and batch installs, and the installation guide lists project prerequisites. Once the file lands it is yours, and Drivn releases never overwrite it.
1 # from the root of your Next.js 16 project 2 npx drivn add card
Render a server-component card grid
Open card.tsx and you will find no 'use client' directive and no hooks — CardRoot is a <div> that merges styles.base with the conditional styles.hover class, so App Router pages render it on the server by default. That lets you map a grid of cards directly inside an async server component that fetches data, and the entire grid ships zero client JavaScript. The base styles fix each card at w-48 aspect-square with bg-card, a border-border border, and a rounded-[20px] radius, plus overflow-hidden so a full-bleed preview image clips to the rounded corners. Wrap the cards in a Tailwind grid and the layout is done — the colors track your theme tokens automatically in dark and light mode.
1 // app/products/page.tsx — async server component, no 'use client' 2 import { Card } from '@/components/ui/card' 3 4 export default async function Page() { 5 const products = await getProducts() 6 return ( 7 <div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> 8 {products.map((product) => ( 9 <Card key={product.id}> 10 <Card.Preview> 11 <img src={product.image} alt={product.name} /> 12 </Card.Preview> 13 <Card.Info> 14 <span className="text-sm font-semibold"> 15 {product.name} 16 </span> 17 </Card.Info> 18 </Card> 19 ))} 20 </div> 21 ) 22 }
Make each card a prefetching link
A card grid usually navigates somewhere on click. Because CardRoot renders a <div> and not a <button>, you can wrap the whole card in a next/link without producing invalid markup — a <Link> renders an <a>, and an anchor wrapping a <div> is valid HTML, unlike nesting one interactive element inside another. The link prefetches the destination route in the background, so the next page is warm before the user clicks. None of this needs a client boundary: Link is server-renderable and the Card carries no handlers, so the clickable grid still ships no card JavaScript. The default hover lift pairs naturally with a link target, signalling the card is interactive. For imperative navigation instead, see the patterns on the Next.js Button guide.
1 import Link from 'next/link' 2 import { Card } from '@/components/ui/card' 3 4 <Link href={`/products/${product.slug}`}> 5 <Card> 6 <Card.Preview> 7 <img src={product.image} alt={product.name} /> 8 </Card.Preview> 9 <Card.Info> 10 <span className="text-sm font-semibold"> 11 {product.name} 12 </span> 13 </Card.Info> 14 </Card> 15 </Link>
Compose the Preview and Info slots
The Card exposes two sub-components through Object.assign(CardRoot, { Preview, Info }), so one import gives you Card, Card.Preview, and Card.Info. Card.Preview is the top region: flex-1 so it grows to fill the square, centered content via flex items-center justify-center, a border-b divider, and p-6 padding — ideal for a thumbnail, illustration, or component preview. Card.Info is the bottom row: p-5 with flex justify-between items-center, which is why a title on the left and a trailing action or badge on the right line up automatically on one row. Both slots accept a className that merges through cn, so you can override padding or alignment per instance without editing the source. The Card examples page shows finished compositions.
1 <Card> 2 <Card.Preview> 3 <span className="text-2xl">��</span> 4 </Card.Preview> 5 <Card.Info> 6 <div> 7 <p className="text-sm font-semibold text-foreground">Card Title</p> 8 <p className="text-xs text-muted-foreground">Description text</p> 9 </div> 10 </Card.Info> 11 </Card>
Disable hover and customize the styles object
The hover prop defaults to true, applying hover:bg-accent hover:border-border hover:-translate-y-1 — a subtle lift and tint that suits a clickable grid. For a static, non-interactive card pass hover={false} and the lift is skipped. Deeper customization is a source edit rather than a prop API: open card.tsx and adjust the styles object, where base holds the size, surface, and radius, preview and info hold the slot layouts. The card is fixed at w-48 and aspect-square, but CardRoot merges any incoming className through cn, so you override the width or aspect per instance — <Card className="w-full aspect-video"> for a wide media card. Because the classes read from your Tailwind tokens, editing the color tokens re-themes every card at once.
1 // static card — no hover lift 2 <Card hover={false}> 3 <Card.Preview> 4 <span className="text-2xl">��</span> 5 </Card.Preview> 6 <Card.Info> 7 <span className="text-sm font-semibold text-foreground"> 8 Card Title 9 </span> 10 </Card.Info> 11 </Card>
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The component file carries no directive, no hooks, and no event handlers — CardRoot is a plain <div> that merges Tailwind classes. It renders as a server component by default, so a grid of cards fetched and rendered on the server ships zero client JavaScript. You only cross the client boundary if you place your own interactive element, like a button with onClick, inside a card slot.
Yes. Wrap the Card in a next/link. Because the card root is a <div> rather than a <button>, an anchor wrapping it is valid HTML and the link prefetches the destination route. No client boundary is required — both Link and Card are server-renderable. Avoid putting a separate button inside the same card if the whole card is already a link, since nested interactive elements confuse keyboard focus.
The base styles fix the card at w-48 aspect-square, but CardRoot merges any className you pass through the cn helper. Pass className="w-full aspect-video" for a wide media card, or set a fixed width to taste. For a permanent change across every card, edit the base key in the styles object at the top of card.tsx — the file lives in your repo after install, so it is a normal source edit.
The Card is pure React and Tailwind. It imports only React and the local cn class-merge helper — no Radix, no cva, no icon library, no animation runtime. The hover lift is a CSS transition, the layout is Tailwind utility classes, and the compound API comes from a single Object.assign. That is why it adds nothing to your client bundle and renders on the server without configuration.

