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
| Prop | Type | Default | Description |
|---|---|---|---|
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, style | native div props | — | Merged 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