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.
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | ReactNode | — | The card's content, typically CardHeader / CardBody / CardFooter. |
| interactive | boolean | — | Adds hover elevation and focus affordances for clickable cards. |
| selected | boolean | — | Applies the selected state with an accent border and tinted background. |
| className | string | — | Extra classes merged onto the card container. |
| CardHeader.title* | ReactNode | — | The header title text (CardHeader). |
| CardHeader.action | ReactNode | — | 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
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 { 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>;
}