Card
Displays a card with header, content, and footer sections.
Preview
Loading...
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
CardAction,
} from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { AspectRatio } from "@/components/ui/AspectRatio";
export function CardDemo() {
return (
<Card className="max-w-sm">
<AspectRatio ratio={16 / 9}>
<img
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
alt="Photo by Drew Beamer"
className="size-full object-cover"
/>
</AspectRatio>
<CardHeader>
<CardTitle>Beautiful Landscape</CardTitle>
<CardDescription>Captured in the mountains</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">
A stunning view of nature's beauty captured at golden hour.
</p>
</CardContent>
<CardFooter>
<Button>View Details</Button>
</CardFooter>
</Card>
)Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/cardManual
Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the Card component into your components/ui/Card.tsx file.
/**
* @description Displays a card with header, content, and footer sections.
* CardAction is the right-hand slot of CardHeader (top right).
*/
import * as React from "react";
import { cn } from "@/utils/class-names";
type CardProps = React.ComponentProps<"div"> & {
/**
* Clickable card. Hover uses a gray-8 border and a soft shadow.
* The card is in the tab order and shows a focus-visible ring.
*/
interactive?: boolean;
};
function Card({
className,
interactive = false,
tabIndex,
onKeyDown,
onClick,
...props
}: CardProps) {
const clickable = interactive || typeof onClick === "function";
return (
<div
data-slot="card"
data-interactive={clickable ? "true" : undefined}
tabIndex={clickable ? (tabIndex ?? 0) : tabIndex}
onClick={onClick}
onKeyDown={
clickable
? (event) => {
onKeyDown?.(event);
if (event.defaultPrevented) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.currentTarget.click();
}
}
: onKeyDown
}
className={cn(
"overflow-hidden border border-gray-6 bg-gray-2 rounded-(--radius-md)",
clickable &&
"cursor-pointer transition-colors motion-reduce:transition-none hover:border-gray-8 hover:shadow-[0_2px_8px_var(--gray-a3)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-8",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"grid auto-rows-min items-start gap-x-1 gap-y-0.5 p-1 pb-0 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
return (
<h3
data-slot="card-title"
className={cn("small m-0 font-semibold text-gray-12", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="card-description"
className={cn("small m-0 text-gray-11", className)}
{...props}
/>
);
}
/**
* Right-hand slot of CardHeader. Place it as a direct child so it sits at the top right, beside the title and description.
*/
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 flex items-center gap-0.5 self-start justify-self-end",
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("small p-1 pt-0 text-gray-12", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center gap-0.5 border-t border-gray-6 p-1",
className
)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};Anatomy
CardAction is the right-hand slot of CardHeader. Place it as a direct child and it sits at the top right, beside the title and description.
Usage
WithFooter
export const WithFooter = () => (
<Card>
<CardHeader>
<CardTitle>Project Setup</CardTitle>
<CardDescription>Configure your project settings</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">
Your project has been created successfully.
</p>
</CardContent>
<CardFooter>
<Button>Continue</Button>
</CardFooter>
</Card>
)WithActions
export const WithActions = () => (
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>You have 3 unread messages</CardDescription>
<CardAction>
<Button variant="menu">Mark all read</Button>
</CardAction>
</CardHeader>
<CardContent>
<div className="space-y-1">
<p className="small text-gray-12">New comment on your post</p>
<p className="small text-gray-11">2 minutes ago</p>
</div>
</CardContent>
</Card>
)SimpleCard
export const SimpleCard = () => (
<Card>
<CardContent>
<p className="small text-gray-12">A simple card with just content.</p>
</CardContent>
</Card>
)MultipleCards
export const MultipleCards = () => (
<div className="flex flex-col gap-1">
<Card>
<CardHeader>
<CardTitle>First Card</CardTitle>
<CardDescription>This is the first card</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">Content for the first card.</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Second Card</CardTitle>
<CardDescription>This is the second card</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">Content for the second card.</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Third Card</CardTitle>
<CardDescription>This is the third card</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">Content for the third card.</p>
</CardContent>
</Card>
</div>
)NestedCards
export const NestedCards = () => (
<Card>
<CardHeader>
<CardTitle>Parent Card</CardTitle>
<CardDescription>This card contains nested cards</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
<Card className="bg-gray-3">
<CardContent>
<p className="small text-gray-12">Nested card 1</p>
</CardContent>
</Card>
<Card className="bg-gray-3">
<CardContent>
<p className="small text-gray-12">Nested card 2</p>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
)InteractiveCard
export const InteractiveCard = () => (
<Card interactive>
<CardHeader>
<CardTitle>Clickable Card</CardTitle>
<CardDescription>Hover to see the effect</CardDescription>
</CardHeader>
<CardContent>
<p className="small text-gray-12">
This card has hover states for interactive experiences.
</p>
</CardContent>
</Card>
)CardGrid
export const CardGrid = () => (
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2 md:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<CardTitle>Card {i + 1}</CardTitle>
</CardHeader>
<CardContent>
<p className="small text-gray-12">Grid item content</p>
</CardContent>
</Card>
))}
</div>
)ImageGrid
export const ImageGrid = () => (
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2 md:grid-cols-3">
{[
{
src: "https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80",
title: "Mountain View",
description: "Scenic landscape",
},
{
src: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&dpr=2&q=80",
title: "Workspace",
description: "Modern office setup",
},
{
src: "https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=800&dpr=2&q=80",
title: "Technology",
description: "Digital innovation",
},
].map((item, i) => (
<Card key={i}>
<AspectRatio ratio={4 / 3}>
<img
src={item.src}
alt={item.title}
className="size-full object-cover"
/>
</AspectRatio>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
)