# Card

> Group related information and actions in a distinct surface.

Source: https://www.honestui.com/docs/components/card

```tsx
"use client";

import * as React from "react";
import { Ellipsis as MoreIcon } from "honestui/icons";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/components/honest-ui/ui/card";
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/components/honest-ui/ui/select";

const frameworkOptions = [
  { label: "Next.js", value: "next" },
  { label: "Vite", value: "vite" },
  { label: "Remix", value: "remix" },
  { label: "Astro", value: "astro" },
];

export function CardDemo() {
  const [status, setStatus] = React.useState("");

  return (
    <Card className="w-full max-w-xs">
      <CardHeader>
        <CardTitle>Create project</CardTitle>
        <CardDescription>
          Choose a name and framework. You can change both later.
        </CardDescription>
        <CardAction>
          <Button
            aria-label="More project options"
            size="icon-sm"
            variant="ghost"
          >
            <MoreIcon aria-hidden="true" />
          </Button>
        </CardAction>
      </CardHeader>
      <Form
        className="grid gap-6"
        onSubmit={(event) => {
          event.preventDefault();
          setStatus("Project details are ready to deploy.");
        }}
      >
        <CardPanel>
          <div className="flex flex-col gap-4">
            <Field>
              <FieldLabel>Name</FieldLabel>
              <FieldControl type="text" placeholder="Name of your project" />
            </Field>
            <Field>
              <FieldLabel>Framework</FieldLabel>
              <Select items={frameworkOptions} defaultValue="next">
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
                <SelectPopup>
                  {frameworkOptions.map(({ label, value }) => (
                    <SelectItem key={value} value={value}>
                      {label}
                    </SelectItem>
                  ))}
                </SelectPopup>
              </Select>
            </Field>
          </div>
        </CardPanel>
        <CardFooter>
          <div className="grid w-full gap-3">
            <Button className="w-full" type="submit">
              Review project
            </Button>
            <p className="text-sm text-muted-foreground" role="status">
              {status}
            </p>
          </div>
        </CardFooter>
      </Form>
    </Card>
  );
}

```

## Overview [#overview]

Use a card to group related content and actions inside a clear surface: a metric with its trend, a billing plan, a team member, a settings group. Cards earn their borders by containing one coherent thing. If a card has three unrelated jobs, split it; if every section of the page becomes a card, the page loses its hierarchy and nothing stands out anymore.

## Anatomy [#anatomy]

A card is a container with an optional header, title, description, action, panel, and footer. `CardHeader` is a CSS grid: title and description stack in the first column, and `CardAction` occupies a second column spanning both rows, so a header button aligns with the title without absolute positioning. Not every card needs every part — omit empty regions instead of preserving a template shape.

## Visual variants [#visual-variants]

Variants change emphasis, not meaning. `default` carries a shadow and ring for primary surfaces; `soft` uses a muted fill for passive grouping; `mixed` adds a border to the muted fill; `outline` is a quiet ring-only surface for secondary content. Pick one hierarchy per view — one `default`-weight card group, softer cards around it — rather than mixing all four at random.

## Composition [#composition]

Keep one idea per card and let the parts carry structure: title names the object, description qualifies it, panel holds the body, footer holds actions. Cards are layout, not behavior. The component renders no interactivity of its own, so any affordance comes from the controls you place inside — see [Accessibility](#accessibility) before making a card itself look clickable.

## Accessibility [#accessibility]

`CardTitle` renders a `div`, not a heading. Screen-reader users navigating by headings will skip right past your cards unless you supply the semantics yourself — put the appropriate heading level inside it, as shown in [Don't do this](#dont-do-this). The same applies to `CardDescription`; it is styled text, not a formal description mechanism.

Avoid nesting interactive targets. A card whose entire area is clickable *and* contains buttons produces mis-taps and ambiguous focus orders. If the card leads somewhere, place one real link with a clear label; if rows of cards are selectable, expose that through a checkbox or radio with its own label instead of a click handler on the container.

Cards have no built-in loading, error, or empty behavior — compose them from other components when needed: a [Skeleton](/docs/components/skeleton) inside `CardPanel` while data loads, an [Empty](/docs/components/empty) state when there is nothing to show yet.

Surfaces, shadows, and text colors come from theme tokens, so cards adapt to dark mode automatically. Content reflows normally at high zoom because the card is plain flexbox with logical padding; very long titles and descriptions wrap rather than truncate.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;card&#x22;]" />
  

  
    
      
        Copy and paste the following code into your project.
      

      ### components/ui/card.tsx

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const cardVariants = cva(
  "relative flex flex-col gap-6 rounded-2xl py-6 text-card-foreground",
  {
    variants: {
      variant: {
        default:
          "bg-card shadow-lg ring-1 shadow-foreground/5 ring-foreground/6.5 dark:shadow-black/10",
        soft: "bg-muted",
        mixed: "border bg-muted",
        outline: "bg-card ring-1 ring-border",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface CardProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof cardVariants> {}

function Card({ className, variant, ...props }: CardProps) {
  return (
    <div
      data-slot="card"
      className={cn(cardVariants({ variant, className }))}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
        className
      )}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-title"
      className={cn("text-lg leading-none font-semibold", className)}
      {...props}
    />
  )
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

function CardAction({ className, ...props }: React.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 CardPanel({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn("px-6", className)}
      {...props}
    />
  )
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
      {...props}
    />
  )
}

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardPanel,
  CardPanel as CardContent,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/components/ui/card";
```

```tsx
<Card>
  <CardHeader>
    <CardTitle>Storage</CardTitle>
    <CardDescription>8.4 GB of 20 GB used</CardDescription>
  </CardHeader>
  <CardPanel>Review the files using the most space.</CardPanel>
  <CardFooter>Manage storage</CardFooter>
</Card>
```

`CardContent` remains available as an alias of `CardPanel` for compatibility with existing code.

## Don't do this [#dont-do-this]

### Titles without heading semantics [#titles-without-heading-semantics]

```tsx
// Bad
<Card>
  <CardHeader>
    <CardTitle>Team plan</CardTitle>
  </CardHeader>
</Card>
```

```tsx
// Good
<Card>
  <CardHeader>
    <CardTitle>
      <h2>Team plan</h2>
    </CardTitle>
  </CardHeader>
</Card>
```

The bad version looks like a heading but is a `div`, so people navigating a page by headings hear nothing about the card. Nesting a real `h1`–`h6` inside keeps the visual design and puts the title on the page's heading map. Pick the level that fits the surrounding document, not the font size.

### Whole-card click handlers [#whole-card-click-handlers]

```tsx
// Bad
<Card onClick={() => router.push(`/projects/${id}`)}>
  <CardHeader>
    <CardTitle>{name}</CardTitle>
  </CardHeader>
  <CardFooter>
    <Button>Open</Button>
  </CardFooter>
</Card>
```

```tsx
// Good
<Card>
  <CardHeader>
    <CardAction>
      <Button render={<Link href={`/projects/${id}`} />}>Open</Button>
    </CardAction>
  </CardHeader>
</Card>
```

A clickable container is invisible to keyboard users (nothing to focus except the inner button, which now does something different from the rest of the card), breaks middle-click and touch scrolling habits, and fails WCAG's target-size expectations once cards sit close together. One explicit link or button says what happens and where.

### Cards inside cards [#cards-inside-cards]

```tsx
// Bad
<Card>
  <CardHeader>
    <CardTitle>Billing</CardTitle>
  </CardHeader>
  <CardPanel>
    <Card variant="outline">
      <CardPanel>$29 / seat</CardPanel>
    </Card>
  </CardPanel>
</Card>
```

```tsx
// Good
<Card>
  <CardHeader>
    <CardTitle>Billing</CardTitle>
    <CardDescription>Team plan · $29 / seat</CardDescription>
  </CardHeader>
  <CardPanel>{/* billing details */}</CardPanel>
</Card>
```

Nesting adds a second border, shadow, and padding scale that compete instead of reinforcing each other, and the reading order gets harder to follow. Flatten the content into the parent card's own regions.

## Examples [#examples]

### Billing plan [#billing-plan]

Variant, pricing panel, and a full-width footer action.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/components/honest-ui/ui/card"

export function CardBillingPlan() {
  return (
    <Card className="w-full max-w-xs" variant="outline">
      <CardHeader>
        <CardTitle>Team plan</CardTitle>
        <CardDescription>For growing product teams.</CardDescription>
      </CardHeader>
      <CardPanel>
        <div className="text-3xl font-semibold">$29<span className="text-sm font-normal text-muted-foreground">/seat</span></div>
      </CardPanel>
      <CardFooter>
        <Button className="w-full">Upgrade</Button>
      </CardFooter>
    </Card>
  )
}

```

### Metric summary [#metric-summary]

A number, a period, and a delta — the smallest useful dashboard card.

```tsx
import { TrendingUp as TrendingUpIcon } from "honestui/icons"

import {
  Card,
  CardDescription,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/components/honest-ui/ui/card"

export function CardMetricSummary() {
  return (
    <Card className="w-full max-w-xs">
      <CardHeader>
        <CardTitle>Revenue</CardTitle>
        <CardDescription>Last 30 days</CardDescription>
      </CardHeader>
      <CardPanel>
        <div className="flex items-end justify-between gap-6">
          <div className="text-3xl font-semibold tracking-tight">$48.2k</div>
          <div className="flex items-center gap-1 text-sm text-success">
            <TrendingUpIcon className="size-4" />
            12.4%
          </div>
        </div>
      </CardPanel>
    </Card>
  )
}

```

### Team member [#team-member]

Avatar, role badges, and two footer actions composed in one surface.

```tsx
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/honest-ui/ui/avatar"
import { Badge } from "@/components/honest-ui/ui/badge"
import { Button } from "@/components/honest-ui/ui/button"
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/honest-ui/ui/card"

export function CardTeamMember() {
  return (
    <Card className="w-full max-w-xs gap-5">
      <CardHeader className="flex flex-row items-start gap-4">
        <Avatar className="size-12 border">
          <AvatarImage
            src="https://images.unsplash.com/photo-1517841905240-472988babdf9?w=96&h=96&fit=crop"
            alt="Riley Scott"
          />
          <AvatarFallback>RS</AvatarFallback>
        </Avatar>
        <div className="min-w-0 flex-1">
          <div className="flex items-start justify-between gap-3">
            <div className="min-w-0">
              <CardTitle>Riley Scott</CardTitle>
              <p className="text-sm text-muted-foreground">Product lead</p>
            </div>
            <Badge variant="success">Online</Badge>
          </div>
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        <p className="text-sm leading-relaxed text-muted-foreground">
          Owns onboarding, activation, and roadmap planning for the growth
          product team.
        </p>
        <div className="flex flex-wrap gap-1.5">
          <Badge variant="secondary">Strategy</Badge>
          <Badge variant="secondary">Research</Badge>
          <Badge variant="secondary">Roadmap</Badge>
        </div>
      </CardContent>
      <CardFooter className="gap-2">
        <Button className="flex-1" size="sm">
          Message
        </Button>
        <Button className="flex-1" variant="secondary" size="sm">
          Profile
        </Button>
      </CardFooter>
    </Card>
  )
}

```

### Resource link [#resource-link]

One clear destination exposed as a real labeled link.

```tsx
import { BookOpen as BookOpenIcon, Link as LinkIcon } from "honestui/icons";

import { Card, CardContent } from "@/components/honest-ui/ui/card";

const item = {
  label: "Documentation",
  description:
    "Find guides, API references, and examples to integrate with our platform.",
  link: "View docs",
  icon: <BookOpenIcon aria-hidden="true" />,
};

export function CardResourceLink() {
  return (
    <Card className="w-full max-w-xs p-0">
      <CardContent className="p-0">
        <div className="border-b px-4 py-3">
          <div className="flex items-center gap-2 text-muted-foreground [&_svg]:size-4">
            {item.icon}
            <span className="text-sm font-medium text-foreground">
              {item.label}
            </span>
          </div>
        </div>
        <div className="space-y-3 p-4">
          <p className="text-sm leading-relaxed text-muted-foreground">
            {item.description}
          </p>
          <Link
            href="/docs/components/card"
            className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
          >
            <LinkIcon aria-hidden="true" className="size-2.5 shrink-0" />
            {item.link}
          </Link>
        </div>
      </CardContent>
    </Card>
  );
}
import Link from "next/link";

```

### Help link [#help-link]

```tsx
import { ExternalLink as ExternalLinkIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/honest-ui/ui/card"

export function CardHelpLink() {
  return (
    <Card className="w-full max-w-xs gap-2 pt-5">
      <CardHeader>
        <CardTitle>Need help with a claim?</CardTitle>
      </CardHeader>
      <CardContent className="mb-2">
        <p>
          Go to this step by step guideline process on how to certify for your
          weekly benefits.
        </p>
      </CardContent>
      <CardFooter className="py-2">
        <Button variant="link" className="px-0">
          See our guideline
          <ExternalLinkIcon aria-hidden="true" />
        </Button>
      </CardFooter>
    </Card>
  )
}

```

## API reference [#api-reference]

`Card` accepts native `div` props plus:

| Prop      | Values                                | Default   |
| --------- | ------------------------------------- | --------- |
| `variant` | `default`, `soft`, `mixed`, `outline` | `default` |

All parts accept native `div` props: `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardPanel`, and `CardFooter`. `CardContent` is an alias of `CardPanel`. None of the parts add semantics beyond their markup — supply headings, labels, and landmarks where the content needs them.
