Swap card
A product card whose photo crossfades and zooms to a second one on hover, with a wobbly badge that turns.
Behaviour
- Hover crossfades to the second image (400ms) while the first zooms in and the second settles from 108% (900ms).
- The badge ring turns 30 degrees and inverts its colours; the title nudges 6px forward (left in RTL), and text starts at the inline start.
- Content sits at the top of the button, so images line up across a grid row whose titles differ in length.
- disabled dims the card and plays no hover state. The focus outline survives forced-colors (high contrast) mode.
- Keyboard focus crossfades the image and inverts the badge plus a focus ring, without the zoom, turn or nudge; a tap gets only the press scale, and hover never sticks after a tap.
- Pure CSS: no JavaScript, nothing per frame, identical server HTML.
- Reduced motion: the images still crossfade and the badge still inverts, but nothing zooms, turns or moves.
Props
| Prop | Type | Description |
|---|---|---|
| image | ReactNode | What the card shows at rest, filling a 448:598 frame. |
| hoverImage | ReactNode | What it crossfades to on hover or keyboard focus. |
| title | ReactNode | The product name under the image. |
| subtitle | ReactNode | The quieter second line (type, price). |
| badge | booleandefault true | The wobbly bag badge in the top-right corner; 22% of the card's width, 36-56px. |
| className | string | Extra classes on the root button; other button props pass through. |
Installation
npx shadcn@latest add https://purrfectui.com/r/swap-card.jsonHow to use
import { SwapCard } from "@/components/ui/swap-card";
<SwapCard
image={<img src="/low-tide.jpg" alt="Low tide" className="size-full object-cover" />}
hoverImage={<img src="/low-tide-detail.jpg" alt="" className="size-full object-cover" />}
title="Low tide"
subtitle="Giclée print"
onClick={() => openProduct("low-tide")}
/>Source code
Loading…
Credits
Ported from poinu.vercel.app.
License
MIT. Copy it into your project and change it as you like.