Card

Bordered surface with optional header, body and footer; interactive and selected states.

import { Card, CardHeader, CardBody, CardFooter }from "@/components/ui"

Specification

Groups related content into a scannable unit. Name a card by its function (Summary, Profile) — not its appearance.

Anatomy

  • Container: surface background, 1px border, 12px radius
  • Header: title + optional action, divided from body
  • Body: 16px padding content area
  • Footer: metadata or actions, divided from body

States

  • Default: surface background, 1px border
  • Hover (interactive): border-hover + elevation
  • Selected: accent border + accent-light tint
  • Loading: skeleton placeholder per content zone

Dimensions & tokens

Radius12px
Border width1px
Padding16px
Content gap8px
Footer gap12px

Do

  • Name the card by what it does, not how it looks
  • Keep consistent padding regardless of content length
  • Clamp long text to preserve grid alignment
  • Make the whole card clickable when it links to detail

Don't

  • Nest cards within cards
  • Mix card variants of different loudness at one level
  • Let variable content break the grid
  • Use cards for every block — sometimes a list suffices

Accessibility

  • If the card is a link, wrap it in <a> (not a div with onClick)
  • Ensure hover treatment is also visible on keyboard focus
  • Provide sr-only context if the title is ambiguous out of context

Spec sourced from mathesis ui-component/content-card

Composed card

Active users

Live

1,284 people have accessed this organisation in the last 30 days.

Full source

The complete component, read from src/components/ui/card.tsx.

card.tsx
import { cn } from "@/lib/utils";

interface CardProps {
  className?: string;
  children: React.ReactNode;

  interactive?: boolean;

  selected?: boolean;
}

export function Card({ className, children, interactive, selected }: CardProps) {
  return (
    <div
      data-selected={selected || undefined}
      className={cn(
        "rounded-xl border border-border bg-surface",
        interactive &&
          "transition-shadow transition-colors hover:border-border-hover hover:shadow-md focus-within:border-accent",
        selected && "border-accent bg-accent-light",
        className
      )}
    >
      {children}
    </div>
  );
}

export interface CardHeaderProps {
  title: React.ReactNode;

  action?: React.ReactNode;
  className?: string;
}

export function CardHeader({ title, action, className }: CardHeaderProps) {
  return (
    <div
      className={cn(
        "flex items-center justify-between gap-3 border-b border-border px-4 py-3",
        className
      )}
    >
      <h3 className="text-xs font-semibold uppercase tracking-wider text-text-secondary">
        {title}
      </h3>
      {action}
    </div>
  );
}

export function CardBody({ className, children }: { className?: string; children: React.ReactNode }) {
  return <div className={cn("p-4", className)}>{children}</div>;
}

export function CardFooter({ className, children }: { className?: string; children: React.ReactNode }) {
  return <div className={cn("border-t border-border px-4 py-3", className)}>{children}</div>;
}