Card
pnpm dlx shadcn@latest add …/r/card.jsonA React card for Next.js and Tailwind CSS: a surface for grouped content with header, action, content and footer parts, a small size and a link style.
- Default
Create project
Deploy a new project in a few clicks. - Small, with an action
Team plan
8 of 10 seats usedActive - Link
Source on GitHub
Browse the code and open issues.
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export function CreateProject() {
return (
<Card>
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy a new project in a few clicks.</CardDescription>
</CardHeader>
<CardContent>{/* fields */}</CardContent>
<CardFooter>
<Button>Deploy</Button>
</CardFooter>
</Card>
)
}import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { ArrowUpRightIcon } from "lucide-react";
import { useId } from "react";
/*
* The range at a glance, each named underneath: a form card with every part, a small card with an action, and a
* whole card as a link (hover it: the surface lifts a step).
*/
export default function CardOverview() {
const id = useId();
return (
<ul className="grid w-full max-w-3xl items-start gap-x-6 gap-y-8 md:grid-cols-2">
<li className="flex flex-col items-center gap-2 md:row-span-2">
<Card className="w-full">
<CardHeader>
<CardTitle asChild>
<h3>Create project</h3>
</CardTitle>
<CardDescription>Deploy a new project in a few clicks.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<label htmlFor={`${id}-name`} className="text-sm font-medium">
Name
</label>
<Input id={`${id}-name`} placeholder="My project" />
</div>
<div className="grid gap-2">
<label htmlFor={`${id}-repo`} className="text-sm font-medium">
Repository
</label>
<Input id={`${id}-repo`} placeholder="github.com/you/project" />
</div>
</CardContent>
<CardFooter className="justify-end">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
<span className="text-xs text-muted-foreground">Default</span>
</li>
<li className="flex flex-col items-center gap-2">
<Card size="sm" className="w-full">
<CardHeader>
<CardTitle asChild>
<h3>Team plan</h3>
</CardTitle>
<CardDescription>8 of 10 seats used</CardDescription>
<CardAction>
<Badge variant="secondary">Active</Badge>
</CardAction>
</CardHeader>
<CardFooter>
<Button variant="outline" size="sm">
Manage seats
</Button>
</CardFooter>
</Card>
<span className="text-xs text-muted-foreground">Small, with an action</span>
</li>
<li className="flex flex-col items-center gap-2">
<Card asChild size="sm" className="w-full">
<a href="https://github.com/FewTone/ui" target="_blank" rel="noreferrer">
<CardHeader>
<CardTitle asChild>
<h3>Source on GitHub</h3>
</CardTitle>
<CardDescription>Browse the code and open issues.</CardDescription>
<CardAction>
<ArrowUpRightIcon aria-hidden className="size-(--size-icon-md) text-muted-foreground" />
</CardAction>
</CardHeader>
</a>
</Card>
<span className="text-xs text-muted-foreground">Link</span>
</li>
</ul>
);
}Installation
Install the dependencies.
$ pnpm add class-variance-authority radix-uiAdd the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.
:root { --size-control-sm: 2rem; --size-control-md: 2.25rem; --size-control-lg: 2.5rem; --size-icon-sm: 0.875rem; --size-icon-md: 1rem; --size-icon-lg: 1.25rem; --size-roll-fade: 0.25em; --size-roll-fade-x: 0.5em; --duration-instant: 80ms; --duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 350ms; --duration-slower: 500ms; --z-overlay: 1200; --z-modal: 1300; --z-popover: 1400; --z-toast: 1500; --z-tooltip: 1600; --focus-ring-width: 2px; --focus-ring-offset: 2px; --opacity-disabled: 0.5; --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1); --destructive-foreground: oklch(1 0 0); --overlay: color-mix(in oklch, oklch(0 0 0) 40%, transparent); } .dark { --destructive-foreground: oklch(1 0 0); --overlay: color-mix(in oklch, oklch(0 0 0) 60%, transparent); } @theme inline { --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both; --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both; --animate-collapsible-open: collapsible-open var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1); --animate-collapsible-close: collapsible-close var(--duration-base) cubic-bezier(0.55, 0, 1, 0.45) both; --color-destructive-foreground: var(--destructive-foreground); --color-overlay: var(--overlay); @keyframes popover-in { from { opacity: 0; filter: blur(6px); transform: scale(0.96); } to { opacity: 1; filter: blur(0); transform: scale(1); } } @keyframes popover-out { from { opacity: 1; filter: blur(0); transform: scale(1); } to { opacity: 0; filter: blur(6px); transform: scale(0.96); } } @keyframes collapsible-open { from { height: 0; opacity: 0; filter: blur(6px); } to { height: var(--collapsible-height); opacity: 1; filter: blur(0); } } @keyframes collapsible-close { from { height: var(--collapsible-height); opacity: 1; filter: blur(0); } to { height: 0; opacity: 0; filter: blur(6px); } } } @media (prefers-reduced-motion: reduce) { :root { --duration-instant: 1ms; --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; --duration-slower: 1ms; } }Copy the source into your project.
import { cn } from "@/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; import { Slot } from "radix-ui"; import type { ComponentProps } from "react"; /* * A surface for grouped content (owner's roadmap, 2026-10-16), with shadcn's parts: Card, CardHeader, CardTitle, * CardDescription, CardAction, CardContent and CardFooter. * - Depth comes from a soft shadow and a hairline, not a border (design.md: few borders), the recipe of the site's * own cards. The hairline is a ring, outside the box, so it never changes the layout. * - As a link (`asChild` with an `a`), the surface lifts a step on hover, to `muted`: colour only, nothing moves. * The whole card is the link, with the focus ring round it, as Badge and Button do as links. * - `size="sm"` tightens the padding and gaps from 24 to 16px, for dense grids such as stats and settings. * - The header is a grid. CardAction takes a second column beside the title and description; that column is * implicit, so it sizes itself to the action without an arbitrary grid template. * - No motion and no "use client": it's a server component. */ const cardVariants = cva( [ "group/card flex flex-col rounded-lg bg-card text-card-foreground shadow-sm ring-1 ring-border", "outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)", // A link card lifts a step on hover: the background only "[a&]:transition-colors [a&]:duration-(--duration-fast) [a&]:ease-out [a&]:hover:bg-muted", ], { variants: { size: { sm: "gap-4 py-4", md: "gap-6 py-6", }, }, defaultVariants: { size: "md" }, }, ); export type CardProps = ComponentProps<"div"> & VariantProps<typeof cardVariants> & { /** Render the single child instead of a div, for example a link, keeping the card styles */ asChild?: boolean; }; /** * A surface for grouped content. It renders a `div`, or with `asChild` its single child, so a link can be a whole * card. Keep a link card short: everything inside it is read as the link's name. */ function Card({ className, size, asChild = false, ...props }: CardProps) { const Comp = asChild ? Slot.Root : "div"; return ( <Comp data-slot="card" data-size={size ?? "md"} className={cn(cardVariants({ size }), className)} {...props} /> ); } function CardHeader({ className, ...props }: ComponentProps<"div">) { return ( <div data-slot="card-header" className={cn( "grid auto-rows-min grid-cols-1 items-start gap-x-4 gap-y-1.5 px-6 group-data-[size=sm]/card:px-4", // With a rule under it (className="border-b"), the header keeps the card's padding below the rule "[.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4", className, )} {...props} /> ); } /** The card's title. A `div` by default; with `asChild`, a heading at the level the page needs (`<h3>`). */ function CardTitle({ className, asChild = false, ...props }: ComponentProps<"div"> & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "div"; return <Comp data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />; } function CardDescription({ className, ...props }: ComponentProps<"div">) { return ( <div data-slot="card-description" className={cn("text-sm text-muted-foreground", className)} {...props} /> ); } /** Sits at the end of the header, beside the title and description: a button, a badge, a menu */ function CardAction({ className, ...props }: ComponentProps<"div">) { return ( <div data-slot="card-action" className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)} {...props} /> ); } function CardContent({ className, ...props }: ComponentProps<"div">) { return ( <div data-slot="card-content" className={cn("px-6 group-data-[size=sm]/card:px-4", className)} {...props} /> ); } function CardFooter({ className, ...props }: ComponentProps<"div">) { return ( <div data-slot="card-footer" className={cn( "flex items-center gap-2 px-6 group-data-[size=sm]/card:px-4", "[.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4", className, )} {...props} /> ); } export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };Update the import paths to match your project.
API reference
Card
The surface: a soft shadow and a hairline, no border. Also accepts every native div prop. Exports cardVariants for styling other elements.
| Prop | Type | Default | Description |
|---|---|---|---|
size? | "sm" | "md" | "md" | Padding and gaps: 24px, or 16px for dense grids. |
asChild? | boolean | false | Render the single child (for example a link) with the card styles. As a link, the surface lifts a step on hover; everything inside is read as the link's name, so keep it short. |
CardHeader
Holds the title, description and action. Add className="border-b" for a rule under it; the padding below follows.
| Prop | Type | Default | Description |
|---|
CardTitle
| Prop | Type | Default | Description |
|---|---|---|---|
asChild? | boolean | false | Render a heading at the level your page needs instead of a div, for example <h3>. |
CardDescription
Muted text under the title.
| Prop | Type | Default | Description |
|---|
CardAction
Sits at the end of the header, beside the title and description: a button, a badge, a menu.
| Prop | Type | Default | Description |
|---|
CardContent
The body, with the card's side padding.
| Prop | Type | Default | Description |
|---|
CardFooter
A row for actions, 8px apart. Add className="border-t" for a rule above it; the padding above follows.
| Prop | Type | Default | Description |
|---|
Related components
An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.
- Stablev2.4.0Beta
An accessible React badge for Next.js and Tailwind CSS: small status labels in five variants, including one with a light travelling round its edge.
- OMJLIN+3
An accessible React avatar for Next.js and Tailwind CSS: a picture or initials in three sizes, alone or in an overlapping group, built on Radix Avatar.