Card

Composable container with optional action area, image, and content sections.

API Reference

Card

Composable container with variant, color, rounded, and padding controls.

PropTypeDefault
variant"outlined" | "solid" | "ghost""outlined"
color"root" | "root-100" | "root-200" | "primary" | "secondary""root"
rounded"none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl""2xl"
padding"none" | "sm" | "md" | "lg""none"
as"div" | "article" | "section" | "li""div"

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.

PropType
hrefstring
onClick() => void

CardContent

Padded section for non-image content. Can appear multiple times within a single Card to create separate content regions.

PropTypeDefault
padding"sm" | "md" | "lg""md"

CardImage

Media wrapper with relative overflow-hidden. Place an Image inside and use rounded to control corner radius independently from the Card.

PropTypeDefault
rounded"none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl""none"

Composition

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 props

Variants & Colors

Outlined

default
image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

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

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

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

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

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>

Rounded

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

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>

Padding

applies to Card directly — use CardContent for padded sections within a composed card
image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

image

Card Title

A short description of the card.

<Card padding="none">...</Card>
<Card padding="sm">...</Card>
<Card padding="md">...</Card>
<Card padding="lg">...</Card>

Basic Usage

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>

Source

src/ui/Card.tsx
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>
  );
}