Card
A surface that groups related content and actions into a single block.
Create project
Projects group your deployments and environments.
Installation
pnpm dlx @dowel-ui/cli add cardnpm packages installed: radix-ui.
Accessibility
Card renders a plain div with no implicit landmark. CardTitle is an h3 by default — use asChild to set the level the page actually needs, since heading levels must increase by one, or to render something that is not a heading at all.
Props
Card
Plus every attribute of <div>.
CardHeader
Plus every attribute of <div>.
CardTitle
| Prop | Type | Default |
|---|---|---|
asChildRenders the child element instead of an | boolean | — |
Plus every attribute of <h3>.
CardDescription
Plus every attribute of <p>.
CardContent
Plus every attribute of <div>.
CardFooter
Plus every attribute of <div>.
Quality
7/7 checks, measured from the source and its tests
- Tested — passes
- axe assertion — passes
- Keyboard tested — does not apply
- Storybook examples — passes
- Accessibility documented — passes
- Semantic tokens only — passes
- Motion from tokens — does not apply
- className merged — passes
- Visible focus — does not apply
- No fixed widths — passes
Used in
Whole screens assembled from this component. Installing one brings this and everything else it needs with it.
- Admin dashboard
- Agent console
- AI dashboard
- Analytics
- Billing
- Command center
- CRM
- CTA banner
- Dashboard
- Features bento
- Features icon grid
- Forgot password
- Login
- Onboarding
- Pricing
- Pricing Single Plan
- Pricing Three Tier
- Pricing Two Tier
- Settings
- Sign up
- Stats grid
- Stats trend cards
Source
This is exactly what dowel add card writes into your project, with imports rewritten to your own path alias.
import { Slot } from "radix-ui";
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils";
/** Surface that groups related content and actions. */
export function Card({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
);
}
export function CardHeader({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="card-header"
className={cn("flex flex-col gap-1.5 px-6 pt-6 pb-4", className)}
{...props}
/>
);
}
export interface CardTitleProps extends ComponentPropsWithRef<"h3"> {
/**
* Renders the child element instead of an `<h3>`.
*
* A card title is an `h3` because that is right in most layouts, and wrong in
* some — heading levels have to increase by one, and a card sitting directly
* under an `h1` needs an `h2`. Use this to set the level the page actually
* requires, or to render something that is not a heading at all.
*/
asChild?: boolean;
}
export function CardTitle({ className, asChild, children, ...props }: CardTitleProps) {
const Comp = asChild ? Slot.Root : "h3";
// `children` is destructured rather than spread so static analysis can see
// that the heading has content — a heading that renders empty is a real
// accessibility defect, and we want the linter able to catch it at call sites.
return (
<Comp
data-slot="card-title"
className={cn("text-lg leading-tight font-semibold tracking-tight", className)}
{...props}
>
{children}
</Comp>
);
}
export function CardDescription({ className, ...props }: ComponentPropsWithRef<"p">) {
return (
<p
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
export function CardContent({ className, ...props }: ComponentPropsWithRef<"div">) {
return <div data-slot="card-content" className={cn("px-6 pb-6", className)} {...props} />;
}
export function CardFooter({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center gap-3 border-t border-border px-6 py-4", className)}
{...props}
/>
);
}