The base card surface with header, content, footer and media parts, an interactive hover-lift mode, and concentric radius handled for nested media.
npx shadcn@latest add @paragon/cardimport * 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>
);
}