Shadcn Card for React and Tailwind
A surface for a single subject: header, media, body, and footer.
Login to your account
Enter your email and check your inbox
Installation
bunx --bun shadcn@latest add https://kit.dev/r/card.jsonpnpm dlx shadcn@latest add https://kit.dev/r/card.jsonnpx shadcn@latest add https://kit.dev/r/card.jsonyarn shadcn@latest add https://kit.dev/r/card.jsonInstall the following dependencies:
bun add @ark-ui/react tailwind-variantspnpm add @ark-ui/react tailwind-variantsnpm install @ark-ui/react tailwind-variantsyarn add @ark-ui/react tailwind-variantsCopy and paste the following code into your project.
import { ark } from "@ark-ui/react/factory";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
export const Card = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn(
"[--space:--spacing(6)]",
"group/card",
"py-(--space)",
"flex flex-col gap-4",
"bg-card",
"text-foreground",
"has-data-[variant=image]:pt-0 has-data-[slot=card-footer]:pb-0",
"rounded-xl border shadow-xs/5",
className
)}
data-slot="card"
{...rest}
/>
);
};
export const cardMediaVariants = tv({
base: [
"flex shrink-0 items-center gap-2",
"[&_svg]:pointer-events-none",
"px-(--space)",
],
variants: {
variant: {
default: "bg-transparent",
icon: "[&_svg:not([class*='size-'])]:size-4",
image: [
"overflow-hidden rounded-t-sm",
"px-0",
"[&_img]:size-full [&_img]:object-cover",
],
},
},
defaultVariants: {
variant: "default",
},
});
export interface CardMediaProps
extends React.ComponentProps<typeof ark.div>,
VariantProps<typeof cardMediaVariants> {}
export const CardMedia = (props: CardMediaProps) => {
const { variant = "default", className, ...rest } = props;
return (
<ark.div
className={cn(cardMediaVariants({ variant }), className)}
data-slot="card-media"
data-variant={variant}
{...rest}
/>
);
};
export interface CardHeaderProps extends React.ComponentProps<typeof ark.div> {
/**
* The description of the card
*/
description?: string;
/**
* The title of the card
*/
title?: string;
}
export const CardHeader = (props: CardHeaderProps) => {
const { title, description, className, children, ...rest } = props;
return (
<ark.div
className={cn(
"grid auto-rows-min grid-rows-[auto_auto] gap-1",
"px-(--space)",
"items-start",
"has-data-[slot=card-action]:grid-cols-[1fr_auto]",
className
)}
data-slot="card-header"
{...rest}
>
{!!title && <CardTitle>{title}</CardTitle>}
{!!description && <CardDescription>{description}</CardDescription>}
{!title && typeof children === "string" ? (
<CardTitle>{children}</CardTitle>
) : (
children
)}
</ark.div>
);
};
export const CardTitle = (props: React.ComponentProps<typeof ark.h2>) => {
const { className, ...rest } = props;
return (
<ark.h2
className={cn(
"font-heading font-semibold text-foreground text-lg/6",
className
)}
data-slot="card-title"
{...rest}
/>
);
};
export const CardDescription = (props: React.ComponentProps<typeof ark.p>) => {
const { className, ...rest } = props;
return (
<ark.p
className={cn("row-start-2", "text-muted-foreground text-sm", className)}
data-slot="card-description"
{...rest}
/>
);
};
export const CardAction = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
data-slot="card-action"
{...rest}
/>
);
};
export const CardContent = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn("px-(--space)", className)}
data-slot="card-content"
{...rest}
/>
);
};
export const CardFooter = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn(
"flex items-center gap-2",
"px-(--space)",
"bg-muted/48",
"rounded-b-xl border-t",
"py-(--space)",
className
)}
data-slot="card-footer"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
Card
├── CardMedia
├── CardHeader
│ ├── CardTitle
│ ├── CardDescription
│ └── CardAction
├── CardContent
└── CardFooterKeep CardHeader, CardContent, and CardFooter as direct children of Card so padding and --space stay aligned.
Usage
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardMedia,
CardTitle,
} from "@/components/ui/card";<Card>
<CardHeader title="Title" description="Description">
<CardAction />
</CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Footer</CardFooter>
</Card>Title and description
CardHeader supports two patterns. Do not mix title / description props with CardTitle / CardDescription children.
Using props
<CardHeader title="Card Title" description="Card description">
<CardAction />
</CardHeader>A string-only child of CardHeader is wrapped in CardTitle when title is omitted.
Using components
Use CardTitle and CardDescription when you need extra nodes or markup.
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description</CardDescription>
<CardAction />
</CardHeader>CardTitle renders an h2. CardDescription renders a p. Use asChild if you need a different heading level.
Examples
Product card
CardMedia with variant="image" sits flush to the top (pt-0 on the card). Give the image an alt.
Icon card
variant="icon" sizes child SVGs to size-4. Decorative icons should be aria-hidden="true".
As a link
asChild merges card styles onto a single child. Do not put buttons inside a link card.
Custom spacing
[--space:--spacing(n)] on Card sets internal padding. Default is --spacing(6).
md:[--space:--spacing(6)] lg:[--space:--spacing(8)]Guides
Spacing
--space is defined on the root and used as py-(--space) and px-(--space) on sections. A footer adds its own py-(--space) and the root drops bottom padding (has-data-[slot=card-footer]:pb-0). Image media drops top padding (has-data-[variant=image]:pt-0).
CardAction
CardAction sits in the header’s end column and spans the title/description rows. Place it as a child of CardHeader.
asChild
<Card asChild>
<a href="/docs">
<CardHeader title="Docs" description="API and examples." />
</a>
</Card>API Reference
shadcn.io Card is composed div / h2 / p (Ark factory), not a Zag machine. There is no context hook or root provider.
asChild merges props onto a single child element.
Card
Root. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. Set [--space:] here. |
| Attribute | Description |
|---|---|
data-slot | card |
| CSS variable | Default | Description |
|---|---|---|
--space | --spacing(6) | Horizontal padding of sections and vertical padding of the root/footer. |
CardMedia
Media strip. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "icon" | "image" | "default" | icon sizes SVGs. image is full-bleed at the top. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the media. |
| Attribute | Description |
|---|---|
data-slot | card-media |
data-variant | "default", "icon", or "image" |
CardHeader
Header grid. Renders a div. With CardAction, the grid is 1fr auto.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | - | Renders CardTitle. Do not also pass CardTitle. |
description | string | - | Renders CardDescription. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the header. |
| Attribute | Description |
|---|---|
data-slot | card-header |
CardTitle
Title. Renders an h2.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child (h3, Link, …). |
className | string | - | Class names on the title. |
| Attribute | Description |
|---|---|
data-slot | card-title |
CardDescription
Supporting text. Renders a p.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the description. |
| Attribute | Description |
|---|---|
data-slot | card-description |
CardAction
End-aligned slot in the header. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the action. |
| Attribute | Description |
|---|---|
data-slot | card-action |
CardContent
Body. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the content. |
| Attribute | Description |
|---|---|
data-slot | card-content |
CardFooter
Footer with a top border and muted background. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the footer. |
| Attribute | Description |
|---|---|
data-slot | card-footer |
cardMediaVariants
Exported tv() recipe. Same variant key as CardMedia.
CardHeaderProps / CardMediaProps
Props of CardHeader and CardMedia.
Accessibility
Card is a layout surface, not a composite widget.
CardTitleis anh2. UseasChildwithh3(or another level) if the page outline needs it.- Images: meaningful
alt, oralt=""if decorative (for example a link card whose title is already in text). - Decorative icons:
aria-hidden="true". - A whole-card link uses
asChildonCard. Do not nest buttons or other links inside it.
Keyboard support
The card is not interactive. If asChild renders a link or button, that control’s keys apply (Tab, Enter). Controls inside the card (inputs, buttons) stay in the normal tab order.