Card
Composable container with optional action area, image, and content sections.
CardActionArea
Stretched click zone that makes the entire card clickable. Renders as a Link when href is passed, or a button when onClick is passed.
| Prop | Type |
|---|---|
| href | string |
| onClick | () => void |
Card
├── CardActionArea optional — wraps any card content to make it clickable (href or onClick)
│ ├── CardImage optional — media wrapper with rounded control
│ └── CardContent optional — padded content section
│ ├── Heading use with explicit color prop
│ └── Text use with explicit color + subtle propsOutlined
defaultCard Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
<Card variant="outlined" color="root">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="outlined" color="root-100">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="outlined" color="root-200">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="outlined" color="primary">
<CardContent>
<Heading size="xs" color="primary">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="outlined" color="secondary">
<CardContent>
<Heading size="xs" color="secondary">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>Solid
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
<Card variant="solid" color="root">
<CardContent>
<Heading size="xs" color="root-contrast">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="solid" color="root-100">
<CardContent>
<Heading size="xs" color="root-100-contrast">Title</Heading>
<Text size="sm" color="root-100-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="solid" color="root-200">
<CardContent>
<Heading size="xs" color="root-200-contrast">Title</Heading>
<Text size="sm" color="root-200-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="solid" color="primary">
<CardContent>
<Heading size="xs" color="primary-contrast">Title</Heading>
<Text size="sm" color="primary-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="solid" color="secondary">
<CardContent>
<Heading size="xs" color="secondary-contrast">Title</Heading>
<Text size="sm" color="secondary-contrast" subtle>Description</Text>
</CardContent>
</Card>Ghost
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
<Card variant="ghost" color="root">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="ghost" color="root-100">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="ghost" color="root-200">
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="ghost" color="primary">
<CardContent>
<Heading size="xs" color="primary">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>
<Card variant="ghost" color="secondary">
<CardContent>
<Heading size="xs" color="secondary">Title</Heading>
<Text size="sm" color="root-contrast" subtle>Description</Text>
</CardContent>
</Card>Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
<Card rounded="none">...</Card>
<Card rounded="sm">...</Card>
<Card rounded="md">...</Card>
<Card rounded="lg">...</Card>
<Card rounded="xl">...</Card>
<Card rounded="2xl">...</Card>
<Card rounded="3xl">...</Card>Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
Card Title
A short description of the card.
<Card padding="none">...</Card>
<Card padding="sm">...</Card>
<Card padding="md">...</Card>
<Card padding="lg">...</Card>import {
Card,
CardActionArea,
CardImage,
CardContent,
Heading,
Text,
Image,
} from "@/ui";
<Card>
<CardActionArea href="/posts/slug">
<CardImage>
<Image src="/cover.jpg" alt="" width={600} height={340} />
</CardImage>
<CardContent>
<Heading size="xs">Title</Heading>
<Text size="sm" subtle>Description</Text>
</CardContent>
</CardActionArea>
</Card>import { cva, type VariantProps } from "class-variance-authority";
import { Link } from "./Link";
import { cn } from "@/utils";
// ── Card ────────────────────────────────────────────────────────────────
export const cardVariants = cva("relative overflow-hidden", {
variants: {
variant: {
solid: "",
outlined: "border",
ghost: "",
},
color: {
root: "",
"root-100": "",
"root-200": "",
primary: "",
secondary: "",
},
rounded: {
none: "",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
xl: "rounded-xl",
"2xl": "rounded-2xl",
"3xl": "rounded-3xl",
},
padding: {
none: "",
sm: "p-3",
md: "p-4",
lg: "p-6",
},
},
compoundVariants: [
// solid
{ variant: "solid", color: "root", className: "bg-root" },
{ variant: "solid", color: "root-100", className: "bg-root-100" },
{ variant: "solid", color: "root-200", className: "bg-root-200" },
{ variant: "solid", color: "primary", className: "bg-primary" },
{ variant: "solid", color: "secondary", className: "bg-secondary" },
// outlined
{ variant: "outlined", color: "root", className: "border-root-contrast" },
{ variant: "outlined", color: "root-100", className: "border-root-100" },
{ variant: "outlined", color: "root-200", className: "border-root-200" },
{ variant: "outlined", color: "primary", className: "border-primary" },
{ variant: "outlined", color: "secondary", className: "border-secondary" },
// ghost
{ variant: "ghost", color: "root", className: "bg-root-ghost" },
{ variant: "ghost", color: "root-100", className: "bg-root-100-ghost" },
{ variant: "ghost", color: "root-200", className: "bg-root-200-ghost" },
{ variant: "ghost", color: "primary", className: "bg-primary-ghost" },
{ variant: "ghost", color: "secondary", className: "bg-secondary-ghost" },
],
defaultVariants: {
variant: "outlined",
color: "root",
rounded: "2xl",
padding: "none",
},
});
export interface CardProps
extends
Omit<React.HTMLAttributes<HTMLElement>, "color">,
VariantProps<typeof cardVariants> {
as?: "div" | "article" | "section" | "li";
}
export function Card({
variant,
color,
rounded,
padding,
as: Tag = "div",
className,
children,
...props
}: CardProps) {
return (
<Tag
className={cn(
cardVariants({ variant, color, rounded, padding }),
className,
)}
{...props}
>
{children}
</Tag>
);
}
// ── CardActionArea ──────────────────────────────────────────────────────
interface CardActionAreaProps extends Omit<
React.HTMLAttributes<HTMLElement>,
"onClick"
> {
href?: string;
onClick?: () => void;
}
export function CardActionArea({
href,
onClick,
className,
children,
...props
}: CardActionAreaProps) {
const classes = cn(
"after:absolute after:inset-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
className,
);
if (href) {
return (
<Link
href={href}
className={cn(classes, "group block")}
{...(props as Record<string, unknown>)}
>
{children}
</Link>
);
}
return (
<button
type="button"
onClick={onClick}
className={cn(classes, "group w-full text-left")}
{...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
>
{children}
</button>
);
}
// ── CardContent ─────────────────────────────────────────────────────────
interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {
padding?: "sm" | "md" | "lg";
}
const contentPadding = {
sm: "p-3",
md: "px-4 py-5",
lg: "px-6 py-8",
} as const;
export function CardContent({
padding = "md",
className,
children,
...props
}: CardContentProps) {
return (
<div className={cn(contentPadding[padding], className)} {...props}>
{children}
</div>
);
}
// ── CardImage ───────────────────────────────────────────────────────────
interface CardImageProps extends React.HTMLAttributes<HTMLDivElement> {
rounded?: "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
}
const imageRounded = {
none: "",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
xl: "rounded-xl",
"2xl": "rounded-2xl",
"3xl": "rounded-3xl",
} as const;
export function CardImage({
rounded = "none",
className,
children,
...props
}: CardImageProps) {
return (
<div
className={cn(
"relative overflow-hidden",
imageRounded[rounded],
className,
)}
{...props}
>
{children}
</div>
);
}