Cards

Card

The base card surface with header, content, footer and media parts, an interactive hover-lift mode, and concentric radius handled for nested media.

Install

npx shadcn@latest add @paragon/card

card.tsx

import * as React from "react";
import { cn } from "@/lib/utils";

export interface CardProps extends React.ComponentProps<"div"> {
  /**
   * Adds hover lift + press feedback for cards that act as a single
   * click target. Pair with an `onClick` or wrap in a link.
   */
  interactive?: boolean;
  /** Disables the lift/press motion while keeping interactive styling. */
  static?: boolean;
}

/**
 * The base card surface: shadow-border depth, card background, 12px radius.
 *
 * `interactive` promotes the surface to a click target — hover raises the
 * shadow and lifts 1px, press settles back down. Both are CSS transitions,
 * so rapid hover-in/out retargets instead of restarting. Nested media should
 * use `CardMedia`, which handles the concentric inner radius.
 */
export function Card({
  interactive = false,
  static: isStatic = false,
  className,
  ...props
}: CardProps) {
  return (
    <div
      data-slot="card"
      data-interactive={interactive || undefined}
      className={cn(
        "flex flex-col rounded-xl bg-card text-card-foreground shadow-border",
        interactive &&
          "cursor-pointer transition-[box-shadow,translate,scale] duration-150 ease-out hover:shadow-border-hover",
        interactive &&
          !isStatic &&
          "motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:active:scale-100 hover:-translate-y-px active:translate-y-0 active:scale-[0.99]",
        className,
      )}
      {...props}
    />
  );
}

export interface CardHeaderProps extends React.ComponentProps<"div"> {}

/** Title/description block with room for an action on the trailing edge. */
export function CardHeader({ className, ...props }: CardHeaderProps) {
  return (
    <div
      data-slot="card-header"
      className={cn("flex flex-col gap-1 px-5 pt-5", className)}
      {...props}
    />
  );
}

export interface CardTitleProps extends React.ComponentProps<"h3"> {}

export function CardTitle({ className, ...props }: CardTitleProps) {
  return (
    <h3
      data-slot="card-title"
      className={cn("text-sm leading-snug font-medium", className)}
      {...props}
    />
  );
}

export interface CardDescriptionProps extends React.ComponentProps<"p"> {}

export function CardDescription({
  className,
  ...props
}: CardDescriptionProps) {
  return (
    <p
      data-slot="card-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  );
}

export interface CardContentProps extends React.ComponentProps<"div"> {}

export function CardContent({ className, ...props }: CardContentProps) {
  return (
    <div
      data-slot="card-content"
      className={cn("px-5 py-4", className)}
      {...props}
    />
  );
}

export interface CardFooterProps extends React.ComponentProps<"div"> {}

/** Action row; add `border-t` yourself when the footer needs separation. */
export function CardFooter({ className, ...props }: CardFooterProps) {
  return (
    <div
      data-slot="card-footer"
      className={cn("flex items-center gap-2 px-5 pb-5", className)}
      {...props}
    />
  );
}

export interface CardMediaProps extends React.ComponentProps<"div"> {}

/**
 * Inset media well. Concentric radius: outer = inner + padding, so with the
 * card at `--radius-xl` and 8px of inset padding the media rounds at
 * `calc(var(--radius-xl) - 8px)` — corners stay parallel instead of pinching.
 */
export function CardMedia({ className, children, ...props }: CardMediaProps) {
  return (
    <div data-slot="card-media" className="p-2 pb-0" {...props}>
      <div
        className={cn(
          "overflow-hidden rounded-[calc(var(--radius-xl)-8px)] bg-muted",
          className,
        )}
      >
        {children}
      </div>
    </div>
  );
}