Installation
npx shadcn@latest add @pixeldosa/cardRequires `cn` at `@/lib/utils`, which `npx shadcn init` creates. CardTitle and CardDescription render `div` elements, not heading tags, matching the wider shadcn/ui convention — supply your own heading element as content if the card needs to participate in the page's document outline. In `orientation="horizontal"`, Card does not auto-arrange its children into an image column and a content column — wrap the header/content/footer parts in your own flex column alongside CardImage.
Usage
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function OrderSummary() {
return (
<Card>
<CardHeader>
<CardTitle>Plated dosa set</CardTitle>
<CardDescription>Serves two, ready in 12 minutes.</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Crisp rice-and-lentil crepe, coconut chutney, and sambar on the side.
</p>
</CardContent>
<CardFooter className="border-t">
<Button className="w-full">Add to order</Button>
</CardFooter>
</Card>
);
}
CardAction places a trailing element — a menu trigger, a badge — in the header's
second column, aligned to the top-right regardless of how much title or description
text wraps:
<CardHeader>
<CardTitle>Plated dosa set</CardTitle>
<CardDescription>Serves two, ready in 12 minutes.</CardDescription>
<CardAction>
<Button variant="ghost" size="icon" aria-label="More options">
<MoreIcon />
</Button>
</CardAction>
</CardHeader>
CardImage adds a full-bleed image region. In the default vertical orientation it
sits first and renders as a top banner; give Card orientation="horizontal" and it
becomes a fixed-width, left-rounded side panel instead — wrap the remaining parts in
your own flex column so they sit beside it:
<Card orientation="horizontal" className="max-w-lg">
<CardImage src="/cabin.jpg" alt="A wooden cabin overlooking a mountain valley" />
<div className="flex flex-1 flex-col justify-between">
<CardHeader>
<CardTitle>Ridgeline cabin</CardTitle>
<CardDescription>Alpine views, sleeps four.</CardDescription>
</CardHeader>
<CardFooter>
<Button size="sm">Reserve now</Button>
</CardFooter>
</div>
</Card>
A pricing card needs none of the above — it's the same Card / CardHeader /
CardTitle / CardDescription / CardContent / CardFooter parts from the first
example, just with a feature list and a price as content. Highlight a tier with a
plain className override (border-2 border-primary) rather than a prop, since
"highlighted" is a one-off treatment, not a structural variant:
<Card className="border-2 border-primary">
<CardHeader>
<CardTitle>Pro</CardTitle>
<CardDescription>For agencies</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<span className="text-3xl font-semibold">$40</span>
<ul className="flex flex-col gap-2 text-sm text-muted-foreground">
<li>Unlimited downloads</li>
<li>Fully editable files</li>
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Subscribe</Button>
</CardFooter>
</Card>
Props
Every part forwards className and all native props of the element it renders.
| Part | Renders | Notes |
|---|---|---|
Card | div | The bordered container. bg-card / text-card-foreground, 1px border-border, rounded-xl. Takes orientation="vertical" | "horizontal" (default vertical). |
CardHeader | div | Grid layout; adds a second column automatically when a CardAction is present. |
CardTitle | div | Not a heading element — nest a real heading tag as its child if the card is part of the page outline. |
CardDescription | div | Muted, small text. |
CardAction | div | Self-aligns to the header's top-right. |
CardContent | div | Horizontal padding only; pairs with Card's vertical py-6. |
CardFooter | div | Add the border-t class yourself when the footer should be visually separated. |
CardImage | img | Full-bleed banner (vertical) or side panel (horizontal) — see above. Requires alt. |
Accessibility
- All parts are plain
divs — Card has no interaction semantics of its own, so there is nothing for Radix to manage here. CardTitledeliberately does not render a heading element, matching the wider shadcn/ui convention. If the card's title should appear in the page's heading outline, render a heading element as its child rather than relying onCardTitleitself for that.- Interactive cards (a whole card acting as a link or button) are not this
component's job — wrap the actionable content in a
Button asChildor an anchor instead of addingonClicktoCarddirectly, so keyboard and screen-reader semantics stay correct. CardImagehas no defaultalt— supply one that describes the image's content. If the image is purely decorative (rare for a listing-style card), passalt=""explicitly rather than omitting the prop.
Engineering Notes
Exposed as eight composable parts rather than a single component with header/footer/title/image props, so a Block composing Card can rearrange or omit regions without fighting a monolithic API. A pricing card, for example, needs zero new Card capability — it composes entirely from the existing header/content/footer parts plus its own feature-list content, which is the intended test of whether something belongs in Card at all versus being a Block built on top of it. CVA now carries exactly one real variant axis, orientation, added because CardImage's rounding genuinely differs between a top banner (vertical) and a side panel (horizontal) — not spending it on density or interactive treatments yet, since no Block currently needs those. Depth uses border-border rather than a shadow, matching the system's dark-mode-first 'borders over shadows' decision documented in the design tokens.
Motion Notes
None. Card is compositional infrastructure with no interaction state of its own — motion belongs on components built on top of it (a future SpotlightCard or an interactive card treatment), not baked into the base primitive, per the pillar rule that motion must earn its place.
Source
The exact file shadcn add writes into your project.
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const cardVariants = cva(
"group flex overflow-hidden rounded-xl border border-border bg-card text-card-foreground",
{
variants: {
/**
* `horizontal` gives Card exactly two flex-row children: a `CardImage` and
* one content column. Header/content/footer parts still stack vertically in
* that case, but Card does not auto-wrap them — group them in your own
* `<div className="flex flex-col">` alongside the image, the same way you'd
* arrange any two-column layout. Card intentionally does not inspect or
* rearrange its children to do this for you.
*/
orientation: {
vertical: "flex-col gap-6 py-6",
horizontal: "flex-row items-stretch gap-0",
},
},
defaultVariants: {
orientation: "vertical",
},
}
);
export interface CardProps
extends React.ComponentPropsWithoutRef<"div">,
VariantProps<typeof cardVariants> {}
/**
* Bordered content container with optional header/content/footer/image regions.
* Depth comes from the 1px border token, not a shadow — matches the system's
* dark-mode-first "borders over shadows" decision so cards read the same weight
* in both themes.
*/
const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(
{ className, orientation, ...props },
ref
) {
return (
<div
ref={ref}
data-slot="card"
data-orientation={orientation ?? "vertical"}
className={cn(cardVariants({ orientation }), className)}
{...props}
/>
);
});
const CardHeader = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardHeader({ className, ...props }, ref) {
return (
<div
ref={ref}
data-slot="card-header"
className={cn(
"grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
"has-[[data-slot=card-action]]:grid-cols-[1fr_auto]",
className
)}
{...props}
/>
);
}
);
/**
* Renders a `div`, not a heading element — matches the wider shadcn/ui convention so
* this drops into an existing project looking identical to the Card a developer
* already knows. If the card participates in the page's document outline, render a
* real heading element as its child instead of relying on this element for semantics.
*/
const CardTitle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardTitle({ className, ...props }, ref) {
return (
<div
ref={ref}
data-slot="card-title"
className={cn("font-semibold leading-none", className)}
{...props}
/>
);
}
);
const CardDescription = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardDescription({ className, ...props }, ref) {
return (
<div
ref={ref}
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
);
/** Self-aligns to the header's top-right — a menu trigger, a badge, a timestamp. */
const CardAction = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardAction({ className, ...props }, ref) {
return (
<div
ref={ref}
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
}
);
const CardContent = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardContent({ className, ...props }, ref) {
return (
<div ref={ref} data-slot="card-content" className={cn("px-6", className)} {...props} />
);
}
);
const CardFooter = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
function CardFooter({ className, ...props }, ref) {
return (
<div
ref={ref}
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
);
/**
* Full-bleed media region. Reads the parent Card's `data-orientation` through the
* `group` class Card carries, so the same element is a top-rounded banner in the
* default vertical layout and a fixed-width, left-rounded side panel in horizontal
* layout — one image component covers both, no separate variant to import.
*/
const CardImage = React.forwardRef<HTMLImageElement, React.ComponentPropsWithoutRef<"img">>(
function CardImage({ className, ...props }, ref) {
return (
<img
ref={ref}
data-slot="card-image"
className={cn(
"-mt-6 aspect-[4/3] w-full shrink-0 rounded-t-xl object-cover",
"group-data-[orientation=horizontal]:mt-0 group-data-[orientation=horizontal]:h-auto",
"group-data-[orientation=horizontal]:w-40 group-data-[orientation=horizontal]:rounded-t-none",
"group-data-[orientation=horizontal]:rounded-l-xl",
className
)}
{...props}
/>
);
}
);
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardAction,
CardContent,
CardFooter,
CardImage,
cardVariants,
};