---
title: Card
description: Title, optional meta, arbitrary body. Heading level is a prop so the outline stays correct.
source: https://adamjurek.com/components/card
---

## Home


Card is a title, optional meta, and whatever body content is passed as
children - the container most content on the site sits in. It grows into an
image card or an icon-tile card from props rather than a `variant` enum, and
renders as a link when given `href`.

<!-- ::start:showcase demo="card" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

An image card and an icon-tile card are not a `variant` prop - they are what
Card grows into when given an `image` or an `icon`, because a card with an
image *is* the image variant. Images crop to a fixed ratio so a grid of cards
holds a line regardless of what was uploaded. The heading level is a prop
(`as`) rather than a fixed `h3`, because a card's place in the document
outline is the page's business, not the card's - getting it wrong is one of
the few styling mistakes a screen reader actually punishes.

## What it does not do

It does not pick its own heading level. The default `h3` assumes the card
sits under an `h2` section; a different nesting needs `as` set explicitly.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/card.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/card.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Containers |
| Files | `card.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | surface, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Card } from "@sushindustries/ui";

export function Example() {
	return (
		<Card title="Accordion" meta="v1.2" icon="rule" tone="motion" href="/components/accordion">
			<p>details, stacked - every row opens on its own.</p>
		</Card>
	);
}
```

## What you should see

A bordered surface with a small toned tile beside the title, "v1.2"
aligned to the top right, and the paragraph below. Because `href` is set
the whole card is a link - hovering it lifts the card and darkens its
background, and the title stays an `h3` since `as` was not passed.

## If nothing happens

A card with no image where one was expected means `image` was left
unset - there is no separate "image variant" prop; passing `image` is
what turns a card into the image card. If cards in a grid do not line up
to the same height, check they sit inside `.card-grid` or an equivalent
grid parent - `Card` itself has no opinion about its siblings.


## Guides


## Composing it

Cards read best inside `.card-grid`, which sizes columns to
`clamp(260px, 30vw, 320px)` and lets them wrap - a single `Card` outside
a grid works fine on its own, but a row of them without the grid class
will not line up. Image cards crop to 16:9 automatically, so uploads of
different sizes still produce a level row.

## The heading level is not decorative

`as` exists because a card sits at different depths in different pages'
outlines - getting it wrong is invisible visually, and flagged by every
outline-based accessibility check.

```tsx
// A grid of cards under a page's own h1
<Card title="Accordion" />

// One card standing in for a whole section's own heading
<Card as="h2" title="Featured" />
```

## When not to use it

`title` is required - a tile that is only an image or only an icon with
no heading belongs in a different component. `Card` assumes a heading is
always present, because the outline argument above depends on one
existing.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `title` | `string` | - |  |
| `meta?` | `string` | - | Shown top-right, in the label style. A version, a date, a count. |
| `children?` | `ReactNode` | - |  |
| `href?` | `string` | - | Renders the card as a link. Omit for a plain container. |
| `as?` | `"h2" \| "h3"` | `"h3"` | Heading level, so a card can sit under the right heading. |
| `image?` | `string` | - | A picture across the top: the image card. The card supplies the frame and the crop; the image supplies everything else, which is why there is no `variant` prop - a card with an image *is* the image variant. |
| `imageAlt?` | `string` | `""` | Alt text for the image. Empty means decorative, which is the default. |
| `icon?` | `IconName` | - | A glyph on a tile beside the title: the category card. |
| `tone?` | `string` | - | Colour family for the icon tile, resolved by the stylesheet. |

<!-- /generated:api -->

## Notes

`image` and `icon` can be combined - the image bleeds across the top and
the icon tile still sits beside the title beneath it - but `imageAlt`
without `image` does nothing, since there is no `<img>` to attach it to.
`href` starting with `http` gets `target="_blank"` and
`rel="noopener noreferrer"` automatically; a relative `href` never does,
so an external link only needs the full URL to get the right behaviour.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="card" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Card } from "@sushindustries/ui";
import { components } from "./components.catalogue";

export function ComponentGrid() {
	return (
		<div className="card-grid">
			{components.map((component) => (
				<Card
					key={component.slug}
					title={component.name}
					icon={component.icon}
					tone={component.category}
					href={`/components/${component.slug}`}
				>
					<p className="fg-dim text-sm">{component.summary}</p>
				</Card>
			))}
		</div>
	);
}
```

## What this example is not

`.card-grid` is what makes the columns line up here - dropping the
individual `Card` elements into a plain `<div>` still renders each one
correctly, but they will not form the even grid this example shows.
