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
Live1,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>;
}