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

Props (8)
Every prop this component accepts, its type, default, and how to use it.
PropTypeDefaultDescription
children*ReactNode—The card's content, typically CardHeader / CardBody / CardFooter.
interactiveboolean—Adds hover elevation and focus affordances for clickable cards.
selectedboolean—Applies the selected state with an accent border and tinted background.
classNamestring—Extra classes merged onto the card container.
CardHeader.title*ReactNode—The header title text (CardHeader).
CardHeader.actionReactNode—Optional trailing action or status shown in the header (CardHeader).
CardBody.children*ReactNode—The padded main content of the card (CardBody).
CardFooter.children*ReactNode—Footer metadata or actions divided from the body (CardFooter).

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 { type ReactNode } from "react";
import { cn } from "@/lib/utils";

interface CardProps {
  className?: string;
  children: 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: ReactNode;

  action?: 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: ReactNode }) {
  return <div className={cn("p-4", className)}>{children}</div>;
}

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