Card
A card groups related content on a raised surface. It is built from parts you put together: a header, a title, a description, content and a footer.
Pro
PopularFor small teams that ship often.
$24/ month
- Up to 10 team members
- Unlimited projects
- Priority support
import { Check } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
const features = ["Up to 10 team members", "Unlimited projects", "Priority support"];
export default function CardDemo() {
return (
<Card className="relative w-full max-w-xs overflow-hidden">
{/* A soft green glow and a bright hairline along the top mark the popular plan. */}
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-36 bg-linear-to-b from-accent/10 to-transparent" />
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-accent to-transparent" />
<CardHeader className="relative">
<div className="flex items-center justify-between gap-3">
<CardTitle>Pro</CardTitle>
<Badge variant="success">Popular</Badge>
</div>
<CardDescription>For small teams that ship often.</CardDescription>
</CardHeader>
<CardContent className="relative flex flex-col gap-5">
<p className="flex items-baseline gap-1.5">
<span className="text-5xl leading-none font-bold tracking-tight">$24</span>
<span className="text-base text-text/70">/ month</span>
</p>
<Separator />
<ul className="flex flex-col gap-3 text-sm text-text/80">
{features.map((feature) => (
<li key={feature} className="flex items-center gap-2.5">
<Check aria-hidden className="size-4 shrink-0 text-accent" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter className="relative">
<Button className="flex-1">Get started</Button>
<Button variant="secondary" className="flex-1">
Learn more
</Button>
</CardFooter>
</Card>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { cx } from "@/lib/cx";
// A card is a raised surface you fill with parts. Build it from Card, CardHeader, CardTitle,
// CardDescription, CardContent and CardFooter; leave out the ones you don't need.
export function Card({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("rounded-lg border border-border bg-surface text-text", className)} {...props} />;
}
export function CardHeader({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex flex-col gap-1.5 p-6", className)} {...props} />;
}
// An h3. If the card sits under a different heading level, change it here.
export function CardTitle({ className, ...props }: ComponentProps<"h3">) {
return <h3 className={cx("text-xl leading-tight font-semibold", className)} {...props} />;
}
export function CardDescription({ className, ...props }: ComponentProps<"p">) {
return <p className={cx("text-sm text-text/70", className)} {...props} />;
}
export function CardContent({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("px-6 pb-6", className)} {...props} />;
}
export function CardFooter({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex items-center gap-3 px-6 pb-6", className)} {...props} />;
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
className | string | — | On every part. Extra classes, added after the part's own. |
Card, CardHeader, CardContent and CardFooter are plain divs. CardTitle is an h3 and CardDescription a paragraph. Every part takes the normal props of its element.
The thinking
A card is a container, so it should have no opinions about what is inside it. That is why it is parts, not props.
- Parts, not props: There is no title or footer prop. You place the parts yourself, so the demo can put a Badge beside the title.
- Leave out what you don't need: A card with only content is fine. Each part adds its own padding, so none depends on another.
- No click behavior: A card isn't a control. If the whole card is a link, put the link on the title or wrap the card, so there is one real link.
Interface design
- Surface: The surface fill with a 1px hairline border and the lg radius, 8px.
- Spacing: The header, content and footer each have 24px of padding at the sides. The footer is a row with 12px between its items.
- Text: The title is 20px semibold. The description is 14px, in the main text color at 70%.
- Dividers: Put a Separator between parts when you want a line, as the demo does.
Interaction
- Not interactive: A card has no hover, focus or pressed state. The buttons and links inside it have their own.
- Focus order: It follows the page order: header, content, then footer, so the buttons in the footer come last.
Empathy
- Choose the heading level: CardTitle is an h3. If the card sits under a different heading, change the level in the file so the page outline stays in order.
- Contrast: The title is 14.2:1 against the surface and the description is 7.5:1.
- One link per card: A card with several links or buttons is fine. A card that is itself a link should have only one.