Card

A surface that groups related content and actions into a single block.

Create project

Projects group your deployments and environments.

Installation

Terminal
pnpm dlx @dowel-ui/cli add card

npm 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

PropTypeDefault
asChild

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.

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

  • Testedpasses
  • axe assertionpasses
  • Keyboard testeddoes not apply
  • Storybook examplespasses
  • Accessibility documentedpasses
  • Semantic tokens onlypasses
  • Motion from tokensdoes not apply
  • className mergedpasses
  • Visible focusdoes not apply
  • No fixed widthspasses

Used in

Whole screens assembled from this component. Installing one brings this and everything else it needs with it.

Source

This is exactly what dowel add card writes into your project, with imports rewritten to your own path alias.

ui/card.tsx
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}
    />
  );
}