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

Popular

For 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

PropTypeDefaultWhat it does
classNamestring—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.