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

PropTypeDescription
imageReactNodeWhat the card shows at rest, filling a 448:598 frame.
hoverImageReactNodeWhat it crossfades to on hover or keyboard focus.
titleReactNodeThe product name under the image.
subtitleReactNodeThe quieter second line (type, price).
badgebooleandefault trueThe wobbly bag badge in the top-right corner; 22% of the card's width, 36-56px.
classNamestringExtra classes on the root button; other button props pass through.

Installation

npx shadcn@latest add https://purrfectui.com/r/swap-card.json

How 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.

0