Card

The card was one of the more interesting cases when I was architecting this component. Originally, I was going to make a record and have defined styles for the card. After further analysis, there could be a case where thousands of cards could be used either for a eCommerece page or blog page or if a user wanted to create a full image card or partial image card. I concluded that component having flexible styling and content would be a better approach given those use cases.

Team Plan

Everything you need to collaborate with your team.

Props & Styling

Props

Card

PropTypeDefaultDescription
size'sm' | 'md' | 'lg' | 'xl'— (required)Sets the --card-px/--card-py CSS custom properties consumed by CardHeader and CardFooter.
variant'bordered' | 'flat' | 'elevated''bordered'Background/border treatment.
behavior'static' | 'interactive''static''interactive' adds a hover lift, shadow, and pointer cursor.
className, stylenative div propsMerged via cn() / spread; style is merged on top of the generated --card-* variables, so you can still override them per-instance.

CardHeader / CardFooter / CardTitle / CardDescription

Plain content wrappers (div, div, h3, p) — no props beyond native HTML attributes plus className. CardHeader/CardFooter read the --card-px/--card-py variables set by the parent Card, so their padding always stays in sync with size without needing a size prop of their own.

Styling

size doesn't set padding directly on Card — it sets CSS custom properties that CardHeader (pt-[var(--card-py)] px-[var(--card-px)]) and CardFooter (pb-[var(--card-py)] px-[var(--card-px)]) read. This is why padding stays consistent even though the card's own body content sits between header and footer with no padding utility of its own — compose your card content between CardHeader and CardFooter rather than adding raw padding to Card itself.

Variants

variant: bordered (default)

Bordered

A simple border, no shadow.

variant: flat

Flat

Muted background, no border.

variant: elevated

Elevated

Border plus a soft shadow.

behavior: interactive

Hover to see the lift + shadow transition.

Interactive

Hover over this card.

Code

import { cn } from "src/lib/utils"

interface CardCSSProperties extends React.CSSProperties {
  "--card-px": string
  "--card-py": string
}

interface CardProps extends React.ComponentPropsWithRef<"div"> {
  variant?: keyof typeof cardStyles.variant;
  behavior?: keyof typeof cardStyles.behavior;
  children: React.ReactNode
  size: "sm" | "md" | "lg" | "xl"
}

const styleMap: Record<CardProps["size"], CardCSSProperties> = {
  sm: {
        "--card-px": "1rem",
        "--card-py": "1rem",
      },
  md: {
      "--card-px": "1.5rem",
      "--card-py": "1.5rem",
      },
  lg: {
      "--card-px": "2rem",
      "--card-py": "2rem",
    },
  xl: {
    "--card-px": "2.5rem",
    "--card-py": "2.5rem",
    }
};

const cardStyles = {
  variant: {
    bordered: "rounded-md border border-border bg-card",
    flat: "rounded-md bg-muted/50 border border-transparent",
    elevated: "rounded-md bg-card border border-border/50 shadow-sm"
  },
  behavior: {
    static: "",
    interactive: "transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md cursor-pointer"
  }
};

const Card = ({
  children,
  ref,
  className,
  style,
  size = "md",
  variant = "bordered",
  behavior = "static",
  ...props} : CardProps) => {
  return (
    <div 
    className = {cn(cardStyles.variant[variant], cardStyles.behavior[behavior], className)}
    style = {{...styleMap[size], ...style}}
    ref = {ref}
    {...props}
    >
      {children}
    </div>
  )
}

export default Card