HextaUI

Card

A surface for grouping content, with three variants, edge-to-edge media, concentric radii and whole-card links.

Used in Pro blocks: Code Block, Diff Review, Stack Trace, and 1 more

Team plan
Billed monthly. Renews on October 28, 2026.

12 of 20 seats used. Invite teammates or remove inactive members to free up seats.

import { IconDotsVertical } from "@tabler/icons-react"

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

export function CardDemo() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Team plan</CardTitle>
        <CardDescription>
          Billed monthly. Renews on October 28, 2026.
        </CardDescription>
        <CardAction>
          <Button variant="ghost" size="icon-sm" aria-label="More options">
            <IconDotsVertical />
          </Button>
        </CardAction>
      </CardHeader>
      <CardContent>
        <p>
          12 of 20 seats used. Invite teammates or remove inactive members to
          free up seats.
        </p>
      </CardContent>
      <CardFooter>
        <Button>Manage seats</Button>
        <Button variant="outline">View invoices</Button>
      </CardFooter>
    </Card>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/card.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardLink,
  CardTitle,
} from "@/components/ui/card"
<Card>
  <CardHeader>
    <CardTitle>Team plan</CardTitle>
    <CardDescription>Billed monthly.</CardDescription>
    <CardAction>
      <Button variant="ghost" size="icon-sm" aria-label="More options">
        <IconDotsVertical />
      </Button>
    </CardAction>
  </CardHeader>
  <CardContent>12 of 20 seats used.</CardContent>
  <CardFooter>
    <Button>Manage seats</Button>
  </CardFooter>
</Card>
Card
├── CardHeader
│   ├── CardTitle
│   │   └── CardLink
│   ├── CardDescription
│   └── CardAction
├── CardContent
└── CardFooter

default sits on a hairline ring with a soft shadow, outline drops the fill and shadow, and muted uses a filled surface for quieter sections.

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

export function CardVariants() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      <Card>
        <CardHeader>
          <CardTitle>Default</CardTitle>
          <CardDescription>Hairline ring and a soft shadow.</CardDescription>
        </CardHeader>
      </Card>
      <Card variant="outline">
        <CardHeader>
          <CardTitle>Outline</CardTitle>
          <CardDescription>Ring only, no fill, no shadow.</CardDescription>
        </CardHeader>
      </Card>
      <Card variant="muted">
        <CardHeader>
          <CardTitle>Muted</CardTitle>
          <CardDescription>Filled surface for quiet sections.</CardDescription>
        </CardHeader>
      </Card>
    </div>
  )
}

size="sm" tightens the spacing, radius and title size together, so a compact card still looks like part of the same family. size="flush" removes the inner spacing for custom layouts such as lists and collapsible rows that bring their own padding.

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

export function CardSizes() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      <Card>
        <CardHeader>
          <CardTitle>Default</CardTitle>
          <CardDescription>Spacing and radius scale together.</CardDescription>
        </CardHeader>
        <CardContent>
          <p>Deploys run on every push to main.</p>
        </CardContent>
        <CardFooter>
          <Button variant="outline">Configure</Button>
        </CardFooter>
      </Card>
      <Card size="sm">
        <CardHeader>
          <CardTitle>Small</CardTitle>
          <CardDescription>Spacing and radius scale together.</CardDescription>
        </CardHeader>
        <CardContent>
          <p>Deploys run on every push to main.</p>
        </CardContent>
        <CardFooter>
          <Button size="sm" variant="outline">
            Configure
          </Button>
        </CardFooter>
      </Card>
    </div>
  )
}

An image, video, picture or aspect ratio placed first or last runs edge to edge. The card drops the padding on that side and clips the media to its corners.

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

export function CardMedia() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      <Card>
        <img
          src="/preview/landscape.svg"
          alt="Mountain landscape"
          className="aspect-video w-full object-cover"
        />
        <CardHeader>
          <CardTitle>Media first</CardTitle>
          <CardDescription>
            The image runs edge to edge and inherits the card corners.
          </CardDescription>
        </CardHeader>
      </Card>
      <Card>
        <CardHeader>
          <CardTitle>Media last</CardTitle>
          <CardDescription>Bottom padding drops for the image.</CardDescription>
        </CardHeader>
        <img
          src="/preview/landscape.svg"
          alt="Mountain landscape"
          className="aspect-video w-full object-cover"
        />
      </Card>
    </div>
  )
}

Put a <CardLink /> in the title to make the whole card one link. Buttons and other controls inside stay clickable and keep their own focus, like the star here.

"use client"

import * as React from "react"
import { IconGitBranch, IconStar } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardLink,
  CardTitle,
} from "@/components/ui/card"

const projects = [
  {
    name: "Acme website",
    description: "Marketing site and docs, deployed on every push.",
    stars: 128,
    branch: "main",
  },
  {
    name: "Payments service",
    description: "Card processing, webhooks and the reconciliation job.",
    stars: 42,
    branch: "release/2.4",
  },
]

export function CardClickable() {
  const [starred, setStarred] = React.useState<string[]>([])

  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      {projects.map((project) => {
        const isStarred = starred.includes(project.name)

        return (
          <Card key={project.name}>
            <CardHeader>
              <CardTitle>
                <CardLink href="#">{project.name}</CardLink>
              </CardTitle>
              <CardDescription>{project.description}</CardDescription>
              <CardAction>
                <Button
                  variant="ghost"
                  size="icon-sm"
                  aria-label={`Star ${project.name}`}
                  aria-pressed={isStarred}
                  onClick={() =>
                    setStarred(
                      isStarred
                        ? starred.filter((name) => name !== project.name)
                        : [...starred, project.name]
                    )
                  }
                >
                  <IconStar className={isStarred ? "fill-current" : ""} />
                </Button>
              </CardAction>
            </CardHeader>
            <CardFooter>
              <span className="flex items-center gap-1 text-muted-foreground">
                <IconGitBranch className="size-4" />
                {project.branch}
              </span>
              <span className="ms-auto text-muted-foreground">
                {project.stars + (isStarred ? 1 : 0)} stars
              </span>
            </CardFooter>
          </Card>
        )
      })}
    </div>
  )
}

Each variant gets its own hover feedback once it holds a link, and the focus ring wraps the whole card when the link is focused with the keyboard.

import { IconArrowUpRight } from "@tabler/icons-react"

import {
  Card,
  CardDescription,
  CardHeader,
  CardLink,
  CardTitle,
} from "@/components/ui/card"

export function CardLinkVariants() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      <Card size="sm">
        <CardHeader>
          <CardTitle>
            <CardLink href="#">
              Default <IconArrowUpRight className="inline size-4" />
            </CardLink>
          </CardTitle>
          <CardDescription>The shadow lifts on hover.</CardDescription>
        </CardHeader>
      </Card>
      <Card variant="outline" size="sm">
        <CardHeader>
          <CardTitle>
            <CardLink href="#">
              Outline <IconArrowUpRight className="inline size-4" />
            </CardLink>
          </CardTitle>
          <CardDescription>A soft fill on hover.</CardDescription>
        </CardHeader>
      </Card>
      <Card variant="muted" size="sm">
        <CardHeader>
          <CardTitle>
            <CardLink href="#">
              Muted <IconArrowUpRight className="inline size-4" />
            </CardLink>
          </CardTitle>
          <CardDescription>The fill deepens on hover.</CardDescription>
        </CardHeader>
      </Card>
    </div>
  )
}

Add border-b to the header or border-t to the footer and the card adds matching padding on the inner side.

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

export function CardBorders() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader className="border-b">
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Choose what you hear about.</CardDescription>
      </CardHeader>
      <CardContent>
        <p>Mentions, replies and deploy failures are on.</p>
      </CardContent>
      <CardFooter className="border-t">
        <Button variant="outline">Reset</Button>
        <Button className="ms-auto">Save</Button>
      </CardFooter>
    </Card>
  )
}

A card inside CardContent derives its radius from the parent’s radius minus the spacing, so the corners stay concentric.

import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardLink,
  CardTitle,
} from "@/components/ui/card"

export function CardNested() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Usage</CardTitle>
        <CardDescription>This billing period.</CardDescription>
      </CardHeader>
      <CardContent>
        <Card variant="muted" size="sm">
          <CardHeader>
            <CardTitle>Build minutes</CardTitle>
            <CardDescription>1,240 of 3,000 used</CardDescription>
          </CardHeader>
        </Card>
        <Card variant="outline" size="sm">
          <CardHeader>
            <CardTitle>
              <CardLink href="#">Bandwidth</CardLink>
            </CardTitle>
            <CardDescription>84 GB of 100 GB used</CardDescription>
          </CardHeader>
        </Card>
      </CardContent>
    </Card>
  )
}

Unbroken titles, long URLs, emoji and CJK text wrap inside a narrow card instead of pushing it wider.

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

export function CardLongContent() {
  return (
    <Card size="sm" className="w-72 max-w-full">
      <CardHeader>
        <CardTitle>
          Supercalifragilisticexpialidocious_unbroken_title_that_never_ends
        </CardTitle>
        <CardDescription>
          https://example.com/a/very/long/path/that/keeps/going/and/going?with=query
        </CardDescription>
        <CardAction>
          <Button variant="outline" size="sm">
            Edit
          </Button>
        </CardAction>
      </CardHeader>
      <CardContent>
        <p>👩‍👩‍👧‍👦 你好世界,这是一个很长的中文句子用于测试换行</p>
      </CardContent>
      <CardFooter>
        <Button variant="outline">Cancel</Button>
        <Button>Continue</Button>
      </CardFooter>
    </Card>
  )
}

Every part takes a render prop, so a card can be an <article> with a real heading, header and footer.

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

export function CardSemantics() {
  return (
    <Card render={<article />} className="w-full max-w-sm">
      <CardHeader render={<header />}>
        <CardTitle render={<h3 />}>Rendered as an article</CardTitle>
        <CardDescription render={<p />}>
          Every part accepts render for real semantics.
        </CardDescription>
      </CardHeader>
      <CardFooter render={<footer />}>
        <Button variant="outline">Read more</Button>
      </CardFooter>
    </Card>
  )
}

The action moves to the inline start and the footer follows the reading direction.

import { IconDotsVertical } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardLink,
  CardTitle,
} from "@/components/ui/card"

export function CardRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <Card>
        <CardHeader>
          <CardTitle>
            <CardLink href="#">موقع أكمي</CardLink>
          </CardTitle>
          <CardDescription>
            موقع تسويقي ووثائق، يُنشر مع كل دفعة.
          </CardDescription>
          <CardAction>
            <Button variant="ghost" size="icon-sm" aria-label="المزيد">
              <IconDotsVertical />
            </Button>
          </CardAction>
        </CardHeader>
        <CardFooter>
          <Button variant="outline">إلغاء</Button>
          <Button>متابعة</Button>
        </CardFooter>
      </Card>
    </div>
  )
}

A card is not focusable on its own. These keys apply when it holds a <CardLink />.

KeyAction
TabFocuses the card link, which draws the focus ring around the whole card, then moves on to each control inside it.
EnterFollows the card link.
  • The link’s accessible name is its own text, so keep <CardLink /> inside the title rather than wrapping the whole card in an anchor.
  • Controls inside a clickable card are lifted above the link’s hit area, so they stay reachable by pointer and keyboard.
  • Use render on <CardTitle /> to give the title the heading level that fits your page.

Every part renders a <div> by default, accepts its attributes and takes a render prop to swap the element.

PropTypeDefault
variant
"default" | "outline" | "muted""default"
sizeScales spacing, radius and title size together.
"default" | "sm" | "flush""default"
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card"Target the card in CSS.
data-variantThe current variant.
data-sizeThe current size.
data-linkPresent while the card contains a CardLink.
--card-spacingInner spacing shared by every part. Set by the size.
--card-radiusCorner radius. Nested cards derive theirs from the parent.
--card-title-sizeFont size used by CardTitle.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-header"A grid that places CardAction in the top end corner.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-title"Target the title.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-description"Target the description.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-action"Spans the title and description rows, above a CardLink’s hit area.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-content"Passes the card radius and spacing down to nested cards.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="card-footer"Target the footer.
PropTypeDefault
href
string–
renderPass your router’s link component here.
ReactElement | (props, state) => ReactElement<a>
AttributeDescription
data-slot="card-link"Stretches its hit area over the whole card.

Used in Pro blocks

Blocks that build on Card.

Every block comes with HextaUI Pro, and Prompt Input is free.