---
title: Layer Card
description: A muted tray holding one or more raised panels, with a header and footer as chrome.
---

```tsx
"use client";

import { ArrowRightIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  LayerCard,
  LayerCardAction,
  LayerCardDescription,
  LayerCardFooter,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const origins = [
  { host: "api.example.com", requests: "1,204" },
  { host: "cdn.example.com", requests: "860" },
  { host: "auth.example.com", requests: "312" },
];

export default function LayerCardDemo() {
  return (
    <LayerCard className="w-full max-w-sm">
      <LayerCardHeader>
        <LayerCardTitle>Subrequests</LayerCardTitle>
        <LayerCardDescription>
          Origins this worker called in the last hour.
        </LayerCardDescription>
        <LayerCardAction>
          <Button variant="ghost" size="icon-sm" aria-label="Open subrequests">
            <ArrowRightIcon />
          </Button>
        </LayerCardAction>
      </LayerCardHeader>
      <LayerCardPanel>
        <ul className="grid gap-2">
          {origins.map((origin) => (
            <li key={origin.host} className="flex items-center justify-between">
              <span className="font-mono text-xs">{origin.host}</span>
              <span className="tabular-nums text-muted-foreground">
                {origin.requests}
              </span>
            </li>
          ))}
        </ul>
      </LayerCardPanel>
      <LayerCardFooter>Showing 3 of 12 origins</LayerCardFooter>
    </LayerCard>
  );
}
```

## Installation

```bash
npx shadcn@latest add @sevenui/layer-card
pnpm dlx shadcn@latest add @sevenui/layer-card
yarn dlx shadcn@latest add @sevenui/layer-card
bunx --bun shadcn@latest add @sevenui/layer-card
```

## Usage

```tsx
import {
  LayerCard,
  LayerCardAction,
  LayerCardDescription,
  LayerCardFooter,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

<LayerCard>
  <LayerCardHeader>
    <LayerCardTitle>Subrequests</LayerCardTitle>
    <LayerCardDescription>Origins called by this worker.</LayerCardDescription>
    <LayerCardAction>…</LayerCardAction>
  </LayerCardHeader>
  <LayerCardPanel>Panel content</LayerCardPanel>
  <LayerCardFooter>Showing 8 of 24</LayerCardFooter>
</LayerCard>;
```

The tray is the root. The header and footer sit on it as chrome, and each
`LayerCardPanel` is a raised surface inside it. Panel corners are derived
from the tray's, so the two arcs stay parallel at any radius.

## Examples

### Surface

Without a panel, the root is a single plain surface.

```tsx
"use client";

import { LayerCard } from "@/components/ui/layer-card";

export default function LayerCardSurface() {
  return (
    <LayerCard className="w-full max-w-xs">
      <p className="text-muted-foreground">Quick start guide for new users.</p>
    </LayerCard>
  );
}
```

### Several panels

A tray can hold any number of panels.

```tsx
"use client";

import { Badge } from "@/components/ui/badge";
import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const deployments = [
  { name: "Production", branch: "main", time: "2 minutes ago", status: "Ready" },
  { name: "Preview", branch: "feat/search", time: "1 hour ago", status: "Building" },
];

export default function LayerCardPanels() {
  return (
    <LayerCard className="w-full max-w-sm">
      <LayerCardHeader>
        <LayerCardTitle>Deployments</LayerCardTitle>
      </LayerCardHeader>
      {deployments.map((deployment) => (
        <LayerCardPanel
          key={deployment.name}
          className="flex items-center justify-between gap-4"
        >
          <div className="grid gap-0.5">
            <span className="font-medium">{deployment.name}</span>
            <span className="text-xs text-muted-foreground">
              {deployment.branch} · {deployment.time}
            </span>
          </div>
          <Badge variant={deployment.status === "Ready" ? "secondary" : "outline"}>
            {deployment.status}
          </Badge>
        </LayerCardPanel>
      ))}
    </LayerCard>
  );
}
```

### Stacked

`stacked` joins adjacent panels with a shared border.

```tsx
"use client";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const services = [
  { name: "Auth service", status: "Healthy · 12 ms p95" },
  { name: "Database", status: "Healthy · 4 ms p95" },
  { name: "Storage", status: "Degraded · 380 ms p95" },
];

export default function LayerCardStacked() {
  return (
    <LayerCard stacked className="w-full max-w-sm">
      <LayerCardHeader>
        <LayerCardTitle>Server logs</LayerCardTitle>
      </LayerCardHeader>
      {services.map((service) => (
        <LayerCardPanel key={service.name} className="grid gap-0.5">
          <span className="font-medium">{service.name}</span>
          <span className="text-xs text-muted-foreground">{service.status}</span>
        </LayerCardPanel>
      ))}
    </LayerCard>
  );
}
```

### Dense

`dense` removes the tray padding so panels run edge to edge. Combine it
with `stacked` for a single joined list.

```tsx
"use client";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const warehouses = [
  { name: "Warehouse A", units: "1,240 units" },
  { name: "Warehouse B", units: "860 units" },
];

function Inventory({ stacked }: { stacked?: boolean }) {
  return (
    <LayerCard dense stacked={stacked}>
      <LayerCardHeader>
        <LayerCardTitle>{stacked ? "Dense, stacked" : "Dense"}</LayerCardTitle>
      </LayerCardHeader>
      {warehouses.map((warehouse) => (
        <LayerCardPanel
          key={warehouse.name}
          className="flex items-center justify-between"
        >
          <span>{warehouse.name}</span>
          <span className="tabular-nums text-muted-foreground">
            {warehouse.units}
          </span>
        </LayerCardPanel>
      ))}
    </LayerCard>
  );
}

export default function LayerCardDense() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <Inventory />
      <Inventory stacked />
    </div>
  );
}
```

### Horizontal

`orientation="horizontal"` lays panels in a row while the header and
footer stay full width. Below 28rem of tray width the panels stack, and
stacked seams turn horizontal.

```tsx
"use client";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const stats = [
  { label: "Requests", value: "1.2M" },
  { label: "Bandwidth", value: "84 GB" },
  { label: "Errors", value: "0.3%" },
];

export default function LayerCardHorizontal() {
  return (
    <LayerCard orientation="horizontal" stacked className="w-full max-w-lg">
      <LayerCardHeader>
        <LayerCardTitle>Usage this month</LayerCardTitle>
      </LayerCardHeader>
      {stats.map((stat) => (
        <LayerCardPanel key={stat.label} className="grid gap-1">
          <span className="text-xs text-muted-foreground">{stat.label}</span>
          <span className="text-2xl font-semibold tabular-nums">
            {stat.value}
          </span>
        </LayerCardPanel>
      ))}
    </LayerCard>
  );
}
```

### Variants

`default` is a muted tray with card panels, `inverse` swaps the two, and
`ghost` drops the tray border.

```tsx
"use client";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const variants = ["default", "inverse", "ghost"] as const;

export default function LayerCardVariants() {
  return (
    <div className="grid w-full max-w-2xl gap-6 sm:grid-cols-3">
      {variants.map((variant) => (
        <LayerCard key={variant} variant={variant}>
          <LayerCardHeader>
            <LayerCardTitle className="capitalize">{variant}</LayerCardTitle>
          </LayerCardHeader>
          <LayerCardPanel className="text-muted-foreground">
            Panel content
          </LayerCardPanel>
        </LayerCard>
      ))}
    </div>
  );
}
```

### Sizes

`size` sets the padding of the panels, header, and footer.

```tsx
"use client";

import {
  LayerCard,
  LayerCardFooter,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const sizes = ["xs", "sm", "default", "lg"] as const;

export default function LayerCardSizes() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      {sizes.map((size) => (
        <LayerCard key={size} size={size}>
          <LayerCardHeader>
            <LayerCardTitle>{size}</LayerCardTitle>
          </LayerCardHeader>
          <LayerCardPanel className="text-muted-foreground">
            Panel content
          </LayerCardPanel>
          <LayerCardFooter>Footer</LayerCardFooter>
        </LayerCard>
      ))}
    </div>
  );
}
```

### Links

Render a panel as a link with `render`. It gains hover and focus styles.

```tsx
"use client";

import { ChevronRightIcon } from "lucide-react";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

const steps = [
  { title: "Install the CLI", description: "Set up shadcn in your project." },
  { title: "Add your first primitive", description: "Copy the source you own." },
  { title: "Customize the theme", description: "Pick colors and radius." },
];

export default function LayerCardLink() {
  return (
    <LayerCard className="w-full max-w-sm">
      <LayerCardHeader>
        <LayerCardTitle>Next steps</LayerCardTitle>
      </LayerCardHeader>
      {steps.map((step) => (
        <LayerCardPanel
          key={step.title}
          render={<a href="#" />}
          className="flex items-center gap-3"
        >
          <div className="grid flex-1 gap-0.5">
            <span className="font-medium">{step.title}</span>
            <span className="text-xs text-muted-foreground">
              {step.description}
            </span>
          </div>
          <ChevronRightIcon className="size-4 text-muted-foreground" />
        </LayerCardPanel>
      ))}
    </LayerCard>
  );
}
```

### Danger zone

`tone="destructive"` tints the tray and its title. Panels stay neutral.

```tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  LayerCard,
  LayerCardDescription,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

export default function LayerCardDestructive() {
  return (
    <LayerCard tone="destructive" stacked className="w-full max-w-md">
      <LayerCardHeader>
        <LayerCardTitle>Danger zone</LayerCardTitle>
        <LayerCardDescription>These actions cannot be undone.</LayerCardDescription>
      </LayerCardHeader>
      <LayerCardPanel className="flex items-center justify-between gap-4">
        <div className="grid gap-0.5">
          <span className="font-medium">Transfer ownership</span>
          <span className="text-xs text-muted-foreground">
            Move this project to another team.
          </span>
        </div>
        <Button variant="outline" size="sm">
          Transfer
        </Button>
      </LayerCardPanel>
      <LayerCardPanel className="flex items-center justify-between gap-4">
        <div className="grid gap-0.5">
          <span className="font-medium">Delete project</span>
          <span className="text-xs text-muted-foreground">
            Remove the project and all of its data.
          </span>
        </div>
        <Button variant="destructive" size="sm">
          Delete
        </Button>
      </LayerCardPanel>
    </LayerCard>
  );
}
```

### Radius

Override `--layer-card-radius` on the root; panels follow. Content inside a
panel can use `--layer-card-panel-radius` to match the panel's corners.

```tsx
"use client";

import {
  LayerCard,
  LayerCardHeader,
  LayerCardPanel,
  LayerCardTitle,
} from "@/components/ui/layer-card";

export default function LayerCardRadius() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <LayerCard className="[--layer-card-radius:var(--radius-sm)]">
        <LayerCardHeader>
          <LayerCardTitle>Small radius</LayerCardTitle>
        </LayerCardHeader>
        <LayerCardPanel className="text-muted-foreground">
          The panel radius follows the tray.
        </LayerCardPanel>
      </LayerCard>
      <LayerCard>
        <LayerCardHeader>
          <LayerCardTitle>Media panel</LayerCardTitle>
        </LayerCardHeader>
        <LayerCardPanel className="overflow-hidden p-0">
          <img
            src="/placeholder.svg"
            alt=""
            className="aspect-video w-full rounded-(--layer-card-panel-radius) object-cover"
          />
        </LayerCardPanel>
      </LayerCard>
    </div>
  );
}
```

## Accessibility

The root is a `div` with no role. To make a titled layer card a labelled
region, render it as a `section` and point it at the title:

```tsx
<LayerCard render={<section aria-labelledby="usage-title" />}>
  <LayerCardHeader>
    <LayerCardTitle render={<h3 id="usage-title" />}>Usage</LayerCardTitle>
  </LayerCardHeader>
  …
</LayerCard>
```

A link panel is one interactive element. Don't nest buttons or other links
inside it.

## API reference

### LayerCard

Extends `div` and accepts `render`.

| Prop          | Type                                     | Default      |
| ------------- | ---------------------------------------- | ------------ |
| `variant`     | `"default" \| "inverse" \| "ghost"`      | `"default"`  |
| `size`        | `"xs" \| "sm" \| "default" \| "lg"`      | `"default"`  |
| `orientation` | `"vertical" \| "horizontal"`             | `"vertical"` |
| `stacked`     | `boolean`                                | `false`      |
| `dense`       | `boolean`                                | `false`      |
| `tone`        | `"default" \| "destructive"`             | `"default"`  |

The root carries `data-slot="layer-card"`, `data-variant`, `data-size`,
`data-orientation`, and `data-tone`, plus `data-stacked` and `data-dense`
when those are set.

### LayerCardPanel

Extends `div` and accepts `render`.

| Prop  | Type      | Default |
| ----- | --------- | ------- |
| `fit` | `boolean` | `false` |

A panel grows into spare space along the tray's direction. `fit` keeps it
at its content size. Rendered as an `a` or `button`, it gets hover and
`focus-visible` styles.

### LayerCardHeader, LayerCardTitle, LayerCardDescription, LayerCardAction, LayerCardFooter

Structural wrappers extending `div`. `LayerCardTitle` also accepts `render`,
for example to pick a heading level. `LayerCardHeader` switches to two
columns when a `LayerCardAction` is present.

### CSS variables

| Variable                    | Default                                 | Description                                        |
| --------------------------- | --------------------------------------- | -------------------------------------------------- |
| `--layer-card-radius`       | `--radius-xl`                           | Tray corner radius. Set it on the root.            |
| `--layer-card-panel-radius` | tray radius minus padding and border    | Panel corner radius, never below 0. Read-only use. |

### Notes

- The tray switches to panel mode when it contains a `LayerCardPanel` at
  any depth, so panels wrapped by `Collapsible` or your own component still
  count. A surface-mode card that directly contains another layered card is
  read as layered.
- A panel that unmounts takes panel mode with it. Base UI's `Collapsible`
  unmounts its panel when closed, so pass `keepMounted` to
  `CollapsibleContent render={<LayerCardPanel />}`; otherwise the tray turns
  into a plain surface every time it closes.
- In surface mode the header and footer drop their own padding, since the
  root already pads the content.
- `stacked` and `dense` only affect panels that are direct children of the
  root.
- In horizontal orientation, `dense` only removes the tray padding, so the
  panels' outer borders sit against the tray border as a double edge. Use
  vertical orientation when you want edge-to-edge panels.
- A horizontal layer card is a size container, so it needs a width from its
  parent. As a flex item without one it collapses; give it `w-full` or a
  `basis-*`.
- Override padding, background, and shadow with ordinary classes
  (`p-6`, `bg-*`, `shadow-none`). To drop the tray border, use
  `variant="ghost"` rather than `border-0`: the panel radius is derived from
  the border width the variant sets.
