Card
A surface in shadcn's parts, plus link, selectable, tilt, spotlight, expandable and stacked cards. Every animation is CSS.
Review order
Check the details before you confirm.
- Plan
- Team, yearly
- Seats
- 12
- Billing email
- billing@acme.co
Installation
Add the registry to components.json, once
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/cardOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/card.jsonThe files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.
Usage
Installed from the registry, import from @/components/ui/card; inside this monorepo, from
@rabtx/ui/card.
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@rabtx/ui/card";
export function Invite() {
return (
<Card>
<CardHeader>
<CardTitle as="h3">Invite your team</CardTitle>
<CardDescription>Teammates can view and edit every project.</CardDescription>
<CardAction>
<Button variant="ghost" size="sm">Later</Button>
</CardAction>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>
<Button size="sm">Send invites</Button>
</CardFooter>
</Card>
);
}The parts are shadcn's, with the same data-slot names, so a shadcn card drops in. Each part pads
itself on the inline axis, so any of them can be left out. CardAction pins to the top right of
the header; the header is a container, and under 15rem wide the action drops below the title.
Media
An img, video, picture or anything with data-slot="card-media" placed directly in the card
runs edge to edge and takes the card's corners when it is first or last.
Studio, Lisbon
Desks for 12 people, booked for the March offsite.
Link card
A card never takes focus itself. Put cardParts.target on the one link or button that owns it:
the target's hit area stretches over the card, and the card answers hover (a stronger edge),
press (a spring down to 0.985) and keyboard focus (the ring is drawn on the card), all through
:has(). Other links and buttons in the card stay on top and keep working.
rabtx/ui
Components on native HTML and CSS.
rabtx/springs
Spring physics solved into CSS easings.
<Card as="article">
<CardHeader>
<CardTitle as="h3">
<a href="/repos/ui" className={cardParts.target}>rabtx/ui</a>
</CardTitle>
</CardHeader>
</Card>Selectable
Make the target a label around a radio or checkbox. A click anywhere on the card picks it, and
the checked input marks the card selected with a --primary edge. A disabled input dims the card
and turns off its hover and press. data-selected, aria-pressed="true" or aria-current on the
target select it too.
Tilt
TiltCard leans toward a mouse or pen in 3D, up to max degrees at a corner (default 10), with a
soft glare under the pointer, and springs back flat when the pointer leaves. Touch, reduced motion
and data-motion="off" keep it flat.
Tilt me
The card leans toward the pointer, with a soft glare.
Steeper, no glare
max is 20 degrees at a corner.
Spotlight
SpotlightCard carries a soft light after the pointer and lights the part of the edge nearest to
it. The light is --primary, so one token recolors a card. When a link or button inside has
keyboard focus, the light rests along the top.
Edge functions
The light follows the pointer and lights the nearest edge.
Analytics
Recolored with one theme token on the card.
Expandable
ExpandableCard is a <details> element: the summary row is the toggle, so the browser provides
the button role, the expanded state, Enter and Space, and opening for find-in-page. A shared
name keeps one card of a group open, natively. The content grows to its height on the panel
spring and the chevron turns over.
Stack
CardStack piles its cards behind the first, each a little smaller, and spreads them into a list.
The toggle is a real button with aria-expanded; while collapsed its hit area covers the pile, so
a click or tap anywhere opens it. A mouse over the pile fans the peeks a little further. Cards of
similar height stack best.
- Deploy finished
web, production, 42s ago
now - Ana commented
“Ship it after the copy pass.”
4m - Invoice paid
INV-2041, $1,840.00
1h - New sign-in
Chrome on macOS, Lisbon
3h
API
Every part takes everything its native element takes. Card adds:
Prop
Type
CardTitle takes as (div, h2 to h6); CardDescription is a p (or div); CardHeader
and CardFooter can render header and footer, which is right inside an article or
section card. TiltCard and SpotlightCard take the Card props, and TiltCard adds:
Prop
Type
ExpandableCard takes everything a <details> takes, plus:
Prop
Type
CardStack takes everything a <section> takes, plus:
Prop
Type
Outside React, cardClass() and cardParts from @rabtx/ui/card/styles give the class names:
<article class="rx-surface rx-card">
<div class="rx-card-header">
<h3 class="rx-card-title"><a class="rx-card-target" href="/a">Title</a></h3>
<p class="rx-card-description">Description</p>
</div>
</article>Theming
The Card reads only the theme tokens.
| Part | Tokens |
|---|---|
| Surface | fill --card, text --card-foreground, Flat edge --border |
| Floating depth | --surface-ring, --surface-edge-top, --surface-edge-bottom, --surface-sheen, --surface-shade |
| Description | --muted-foreground |
| Hover (link, selectable, expandable) | edge --input, ring mixed toward --card-foreground |
| Selected | --primary |
| Focus | --ring |
| Tilt glare | --card-foreground at 10% |
| Spotlight | --primary |
| Corners | --radius-box |
| Motion | --spring-press, --spring-magnetic, --spring-panel, --spring-swap, --motion-ease |
How the motion works
| Effect | CSS |
|---|---|
| Interactive card | .rx-card:has(.rx-card-target) gets hover inside @media (hover: hover) and (pointer: fine), scale: 0.985 while the target is :active on the press spring, and the focus ring when the target is :focus-visible |
| Selected | :has(.rx-card-target :checked) swaps the edge variables to --primary |
| Tilt | the pointer writes --_tilt-x and --_tilt-y; transform: perspective() rotateX() rotateY() transitions on the magnetic spring; the glare is a radial gradient at --_px / --_py under the content |
| Spotlight | two pseudo-elements at --_px / --_py: a wide radial light inside, and a narrower one on a 1px padded layer whose content box is masked out (mask-composite: exclude), so only the edge lights |
| Expandable | interpolate-size: allow-keywords lets ::details-content transition block-size from 0 to auto on the panel spring, with content-visibility kept until it ends (allow-discrete) |
| Stack | each item is a one-row grid with its card aligned to the bottom (align-items: unsafe end); collapsed an item is 10px tall so its card overflows upward behind the one before, open it grows to auto with interpolate-size, and the cards scale back from 1 - i × 0.05 |
| Chevrons | rotate on the swap spring |
JavaScript only writes the pointer position and the expanded state. The springs are fixed
linear() curves: a tilt that is interrupted restarts from where it is.
Browser support
- Chromium 131+: everything, including the expanding details and stack heights.
- Safari and Firefox: cards, link and selectable cards, tilt and spotlight work; without
interpolate-sizeand::details-contentthe expandable card and the stack change height at once instead of animating.
Accessibility
- A card adds no role and no tab stop. Use
as="article"or a labelledsectionwhere the card is a unit of its own, andCardTitle asfor the right heading level. - A clickable card is a real link, button or label inside it, so its name, role and keyboard come from that element. Name a link by its text, not "Read more".
ExpandableCardis nativedetailsandsummary. The stack's toggle is a button witharia-expandedandaria-controls, and the cards behind areinertwhile collapsed.- Tilt and spotlight are decoration: they ignore touch, and reduced motion or
data-motion="off"keeps the card flat and drops every fade and height animation.
Input
A native text input drawn like the Select trigger, with icons, addons, a floating label, clear and password buttons, and a one-time code. Every animation is CSS.
Dialog
A native modal dialog in shadcn's parts, with sizes, right and bottom sheets and a morph from its trigger. Every animation is CSS.