---
type: doc
title: Card
description: Displays a card with header, content, and footer.
---

> Fulldev UI for Astro. The `@fulldev` registry in `components.json` points at `https://ui.full.dev/r/styles/{style}/{name}.json`; see [installation](/astro/docs/installation.md).

```astro title="src/components/examples/card/card-demo.astro"
---
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 { Label } from "@/components/ui/label"
---

<Card class="w-full max-w-sm">
  <CardHeader>
    <CardTitle>Login to your account</CardTitle>
    <CardDescription>
      Enter your email below to login to your account
    </CardDescription>
    <CardAction>
      <Button variant="link">Sign Up</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <form id="card-demo-login">
      <div class="flex flex-col gap-6">
        <div class="grid gap-2">
          <Label for="email">Email</Label>
          <Input id="email" type="email" placeholder="m@example.com" required />
        </div>
        <div class="grid gap-2">
          <div class="flex items-center">
            <Label for="password">Password</Label>
            <a
              href="#"
              class="ml-auto inline-block text-sm underline-offset-4 hover:underline"
            >
              Forgot your password?
            </a>
          </div>
          <Input id="password" type="password" required />
        </div>
      </div>
    </form>
  </CardContent>
  <CardFooter class="flex-col gap-2">
    <Button type="submit" form="card-demo-login" class="w-full">
      Login
    </Button>
    <Button variant="outline" class="w-full">
      Login with Google
    </Button>
  </CardFooter>
</Card>
```

## Installation

```bash
npx shadcn@latest add @fulldev/card
```

## Usage

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

```astro
<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card Description</CardDescription>
    <CardAction>Card Action</CardAction>
  </CardHeader>
  <CardContent>
    <p>Card Content</p>
  </CardContent>
  <CardFooter>
    <p>Card Footer</p>
  </CardFooter>
</Card>
```

### Polymorphic

`Card` and `CardTitle` are polymorphic via `as`. Both render a `div` by
default. A `dl` card holds `dt` and `dd` pairs, optionally wrapped in a `div`.
Keep other links and buttons out of a link card.

```astro
<Card as="article">...</Card>
<Card as="section" aria-labelledby="plan-title">
  ...
</Card>
<Card as="a" href="/pricing">
  ...
</Card>
<Card as="li">...</Card>
<Card as="dl">
  <div>
    <dt>Plan</dt>
    <dd>Pro</dd>
  </div>
</Card>

<CardTitle as="h3">Card Title</CardTitle>
```

## Composition

```text
Card
├── CardHeader
│   ├── CardTitle
│   ├── CardDescription
│   └── CardAction
├── CardContent
└── CardFooter
```

## Size

Use `size="sm"` for a small card with tighter spacing.

```astro title="src/components/examples/card/card-small.astro"
---
import ChevronRightIcon from "@lucide/astro/icons/chevron-right"

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

const featureName = "Scheduled reports"
---

<Card size="sm" class="mx-auto w-full max-w-xs">
  <CardHeader>
    <CardTitle>{featureName}</CardTitle>
    <CardDescription>Weekly snapshots. No more manual exports.</CardDescription>
  </CardHeader>
  <CardContent>
    <ul class="grid gap-2 py-2 text-sm">
      <li class="flex gap-2">
        <ChevronRightIcon class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
        <span>Choose a schedule (daily, or weekly).</span>
      </li>
      <li class="flex gap-2">
        <ChevronRightIcon class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
        <span>Send to channels or specific teammates.</span>
      </li>
      <li class="flex gap-2">
        <ChevronRightIcon class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
        <span>Include charts, tables, and key metrics.</span>
      </li>
    </ul>
  </CardContent>
  <CardFooter class="flex-col gap-2">
    <Button size="sm" class="w-full">
      Set up scheduled reports
    </Button>
    <Button variant="outline" size="sm" class="w-full">
      See what&apos;s new
    </Button>
  </CardFooter>
</Card>
```

## Spacing

Set the `--card-spacing` CSS variable to control the gap between sections and
the inset of card parts.

```astro title="src/components/examples/card/card-spacing.astro"
---
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 { Label } from "@/components/ui/label"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const spacingOptions = [
  {
    className: "[--card-spacing:--spacing(4)]",
    label: "16px",
    value: "4",
  },
  {
    className: "[--card-spacing:--spacing(5)]",
    label: "20px",
    value: "5",
  },
  {
    className: "[--card-spacing:--spacing(6)]",
    label: "24px",
    value: "6",
  },
  {
    className: "[--card-spacing:--spacing(8)]",
    label: "32px",
    value: "8",
  },
]

const spacing = "4"
const selectedSpacing = spacingOptions.find(
  (option) => option.value === spacing
)
---

<div
  class="mx-auto grid w-full max-w-sm gap-4"
  data-card-spacing={JSON.stringify(spacingOptions)}
>
  <ToggleGroup
    defaultValue={spacing}
    variant="outline"
    size="sm"
    class="justify-center"
  >
    {spacingOptions.map((option) => (
      <ToggleGroupItem value={option.value}>{option.label}</ToggleGroupItem>
    ))}
  </ToggleGroup>
  <Card class={selectedSpacing?.className} data-value={spacing}>
    <CardHeader>
      <CardTitle>Login to your account</CardTitle>
      <CardDescription>
        Enter your email below to login to your account
      </CardDescription>
      <CardAction>
        <Button variant="link">Sign Up</Button>
      </CardAction>
    </CardHeader>
    <CardContent>
      <form id="card-spacing-login">
        <div class="flex flex-col gap-6">
          <div class="grid gap-2">
            <Label for="email-spacing">Email</Label>
            <Input
              id="email-spacing"
              type="email"
              placeholder="m@example.com"
              required
            />
          </div>
          <div class="grid gap-2">
            <div class="flex items-center">
              <Label for="password-spacing">Password</Label>
              <a
                href="#"
                class="ml-auto inline-block text-sm underline-offset-4 hover:underline"
              >
                Forgot your password?
              </a>
            </div>
            <Input id="password-spacing" type="password" required />
          </div>
        </div>
      </form>
    </CardContent>
    <CardFooter class="flex-col gap-2">
      <Button type="submit" form="card-spacing-login" class="w-full">
        Login
      </Button>
      <Button variant="outline" class="w-full">
        Login with Google
      </Button>
    </CardFooter>
  </Card>
</div>

<script>
  type SpacingOption = { className: string; value: string }

  const initialize = () => {
    for (const root of document.querySelectorAll<HTMLElement>(
      "[data-card-spacing]"
    )) {
      if (root.hasAttribute("data-bound")) continue
      root.setAttribute("data-bound", "")
      const options = JSON.parse(root.dataset.cardSpacing!) as SpacingOption[]
      const group = root.querySelector('[data-slot="toggle-group"]')!
      const card = root.querySelector<HTMLElement>('[data-slot="card"]')!

      group.addEventListener("toggle-group:change", (event) => {
        const [value] = (event as CustomEvent<{ value: string[] }>).detail.value
        // Keep one spacing selected, as the controlled React example does.
        if (!value) {
          group.dispatchEvent(
            new CustomEvent("toggle-group:set", {
              detail: { value: card.dataset.value },
            })
          )
          return
        }
        for (const option of options) card.classList.remove(option.className)
        const selected = options.find((option) => option.value === value)
        if (selected) card.classList.add(selected.className)
        card.dataset.value = value
      })
    }
  }

  initialize()
  document.addEventListener("astro:page-load", initialize)
</script>
```

Use `-mx-(--card-spacing)` to make content go edge to edge, and
`-mb-(--card-spacing)` on `CardContent` when that content sits above a footer.

```astro title="src/components/examples/card/card-edge-to-edge.astro"
---
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
---

<Card class="mx-auto w-full max-w-sm">
  <CardHeader>
    <CardTitle>Terms of Service</CardTitle>
    <CardDescription>
      Review the terms before accepting the agreement.
    </CardDescription>
  </CardHeader>
  <CardContent class="-mb-(--card-spacing)">
    <div class="-mx-(--card-spacing) max-h-48 space-y-4 overflow-y-scroll border-t bg-muted/50 px-(--card-spacing) py-4 text-sm leading-relaxed">
      <p>
        These terms govern your use of the workspace, including access to shared
        documents, project files, and collaboration tools.
      </p>
      <p>
        You are responsible for the content you upload and for ensuring that
        your team has the appropriate permissions to view or edit it.
      </p>
      <p>
        We may update features or limits as the service evolves. When those
        changes materially affect your workflow, we will notify your workspace
        administrators.
      </p>
      <p>
        By continuing, you agree to keep your account credentials secure and to
        follow your organization&apos;s acceptable use policies.
      </p>
    </div>
  </CardContent>
  <CardFooter class="justify-end gap-2">
    <Button variant="outline">Decline</Button>
    <Button>Accept</Button>
  </CardFooter>
</Card>
```

## Image

Add an image before the card header to create a card with an image.

```astro title="src/components/examples/card/card-image.astro"
---
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
---

<Card class="relative mx-auto w-full max-w-sm pt-0">
  <div class="absolute inset-0 z-30 aspect-video bg-black/35"></div>
  <img
    src="https://avatar.vercel.sh/shadcn1"
    alt="Event cover"
    class="relative z-20 aspect-video w-full object-cover brightness-60 grayscale dark:brightness-40"
  />
  <CardHeader>
    <CardAction>
      <Badge variant="secondary">Featured</Badge>
    </CardAction>
    <CardTitle>Design systems meetup</CardTitle>
    <CardDescription>
      A practical talk on component APIs, accessibility, and shipping faster.
    </CardDescription>
  </CardHeader>
  <CardFooter>
    <Button class="w-full">View Event</Button>
  </CardFooter>
</Card>
```

## RTL

```astro title="src/components/examples/card/card-rtl.astro"
---
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 { Label } from "@/components/ui/label"

const translations = {
  en: {
    dir: "ltr",
    values: {
      title: "Login to your account",
      description: "Enter your email below to login to your account",
      signUp: "Sign Up",
      email: "Email",
      emailPlaceholder: "m@example.com",
      password: "Password",
      forgotPassword: "Forgot your password?",
      login: "Login",
      loginWithGoogle: "Login with Google",
    },
  },
  ar: {
    dir: "rtl",
    values: {
      title: "تسجيل الدخول إلى حسابك",
      description: "أدخل بريدك الإلكتروني أدناه لتسجيل الدخول إلى حسابك",
      signUp: "إنشاء حساب",
      email: "البريد الإلكتروني",
      emailPlaceholder: "m@example.com",
      password: "كلمة المرور",
      forgotPassword: "نسيت كلمة المرور؟",
      login: "تسجيل الدخول",
      loginWithGoogle: "تسجيل الدخول باستخدام Google",
    },
  },
  he: {
    dir: "rtl",
    values: {
      title: "התחבר לחשבון שלך",
      description: "הזן את האימייל שלך למטה כדי להתחבר לחשבון שלך",
      signUp: "הירשם",
      email: "אימייל",
      emailPlaceholder: "m@example.com",
      password: "סיסמה",
      forgotPassword: "שכחת את הסיסמה?",
      login: "התחבר",
      loginWithGoogle: "התחבר עם Google",
    },
  },
}

const { dir, values: t } = translations.ar
---

<Card class="w-full max-w-sm" dir={dir}>
  <CardHeader>
    <CardTitle>{t.title}</CardTitle>
    <CardDescription>{t.description}</CardDescription>
    <CardAction>
      <Button variant="link">{t.signUp}</Button>
    </CardAction>
  </CardHeader>
  <CardContent>
    <form id="card-rtl-login">
      <div class="flex flex-col gap-6">
        <div class="grid gap-2">
          <Label for="email-rtl">{t.email}</Label>
          <Input
            id="email-rtl"
            type="email"
            placeholder={t.emailPlaceholder}
            required
          />
        </div>
        <div class="grid gap-2">
          <div class="flex items-center">
            <Label for="password-rtl">{t.password}</Label>
            <a
              href="#"
              class="ms-auto inline-block text-sm underline-offset-4 hover:underline"
            >
              {t.forgotPassword}
            </a>
          </div>
          <Input id="password-rtl" type="password" required />
        </div>
      </div>
    </form>
  </CardContent>
  <CardFooter class="flex-col gap-2">
    <Button type="submit" form="card-rtl-login" class="w-full">
      {t.login}
    </Button>
    <Button variant="outline" class="w-full">
      {t.loginWithGoogle}
    </Button>
  </CardFooter>
</Card>
```

## API Reference

Source: [`src/components/ui/card`](https://github.com/fulldotdev/ui/tree/main/src/components/ui/card)

### Card

| Prop   | Type                | Default     |
| ------ | ------------------- | ----------- |
| `size` | `"default" \| "sm"` | `"default"` |
| `as`   | HTML tag            | `"div"`     |

### CardTitle

| Prop | Type     | Default |
| ---- | -------- | ------- |
| `as` | HTML tag | `"div"` |

`CardHeader`, `CardDescription`, `CardAction`, `CardContent` and `CardFooter`
render a `div` and take its attributes.

## Install what this page shows

```bash
npx shadcn@latest add @fulldev/card-examples
```
