Expand Card
Original · freeA card that expands in place to reveal more content.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-expand.json"use client";
import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Plan = {
id: string;
name: string;
price: string;
cadence: string;
tagline: string;
img: string;
highlights: string[];
includes: string[];
cta: string;
};
const PLANS: Plan[] = [
{
id: "atlas-pro",
name: "Atlas Pro Keyboard",
price: "$189",
cadence: "one-time",
tagline: "75% hot-swap board with gasket mount and PBT caps.",
img: "/img/gallery/g07.webp",
highlights: [
"Gasket-mounted plate for a soft, cushioned typing feel",
"South-facing hot-swap sockets, 3- and 5-pin compatible",
"Tri-mode: USB-C, Bluetooth 5.1, and 2.4 GHz dongle",
],
includes: ["Coiled aviator cable", "Keycap + switch puller", "2-year warranty"],
cta: "Add to cart",
},
{
id: "north-desk",
name: "North Standing Desk",
price: "$429",
cadence: "one-time",
tagline: "Dual-motor sit-stand frame with a solid oak top.",
img: "/img/gallery/g14.webp",
highlights: [
"Lifts 265 lb at 1.5 in/sec across a 25–51 in range",
"Four programmable height presets with anti-collision",
"FSC-certified oak, hand-finished with a matte hardwax oil",
],
includes: ["Cable tray + sleeve", "Grommet + monitor arm mount", "10-year frame warranty"],
cta: "Configure desk",
},
{
id: "lumen-lamp",
name: "Lumen Task Lamp",
price: "$96",
cadence: "one-time",
tagline: "CRI-95 LED bar with a weighted brass base.",
img: "/img/gallery/g22.webp",
highlights: [
"Stepless dimming from 2700K warm to 5000K daylight",
"Auto-adapts brightness to ambient light in the room",
"Solid brass base — no clamp, no wobble, no flicker",
],
includes: ["USB-C pass-through port", "Memory for last setting", "5-year warranty"],
cta: "Add to cart",
},
];
const EASE = [0.22, 1, 0.36, 1] as const;
function ChevronIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M6 9l6 6 6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M20 6L9 17l-5-5"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function BoxIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M21 8l-9-5-9 5m18 0l-9 5m9-5v8l-9 5m0-8L3 8m9 5v8M3 8v8l9 5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function HeartIcon({ filled, className }: { filled: boolean; className?: string }) {
return (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} aria-hidden="true" className={className}>
<path
d="M20.8 6.6a5.2 5.2 0 00-8.8-2.9 5.2 5.2 0 00-8.8 3.7c0 4.5 5.9 8.9 8.8 10.9 2.9-2 8.8-6.4 8.8-11a5 5 0 000-.7z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
);
}
function ExpandCard({ plan }: { plan: Plan }) {
const [open, setOpen] = useState(false);
const [liked, setLiked] = useState(false);
const reduce = useReducedMotion();
const panelId = useId();
const btnId = useId();
return (
<div
className={[
"group relative flex flex-col overflow-hidden rounded-3xl text-left",
"border border-slate-200/80 bg-white shadow-sm",
"dark:border-white/10 dark:bg-slate-900",
"transition-[box-shadow,transform,border-color] duration-300",
"hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-900/5",
open ? "ring-1 ring-indigo-500/30 dark:ring-indigo-400/30" : "",
].join(" ")}
>
<div className="relative aspect-[16/10] w-full overflow-hidden bg-slate-100 dark:bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={plan.img}
alt={`${plan.name} product photo`}
loading="lazy"
draggable={false}
className="h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.04]"
/>
<span className="absolute left-3 top-3 rounded-full bg-white/85 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-700 backdrop-blur dark:bg-slate-900/70 dark:text-slate-200">
In stock
</span>
<button
type="button"
aria-pressed={liked}
aria-label={liked ? `Remove ${plan.name} from wishlist` : `Add ${plan.name} to wishlist`}
onClick={() => setLiked((v) => !v)}
className={[
"absolute right-3 top-3 grid h-9 w-9 place-items-center rounded-full backdrop-blur",
"transition-colors duration-200 outline-none",
"focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
liked
? "bg-rose-500 text-white"
: "bg-white/85 text-slate-600 hover:text-rose-500 dark:bg-slate-900/70 dark:text-slate-300",
].join(" ")}
>
<HeartIcon filled={liked} className="h-4 w-4" />
</button>
</div>
<div className="flex flex-1 flex-col p-5">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{plan.name}</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{plan.tagline}</p>
</div>
<div className="shrink-0 text-right">
<div className="text-lg font-bold text-slate-900 dark:text-white">{plan.price}</div>
<div className="text-[11px] font-medium uppercase tracking-wide text-slate-400">{plan.cadence}</div>
</div>
</div>
<button
id={btnId}
type="button"
aria-expanded={open}
aria-controls={panelId}
onClick={() => setOpen((v) => !v)}
className={[
"mt-4 flex w-full items-center justify-between gap-2 rounded-xl px-3 py-2.5 text-sm font-medium",
"text-slate-700 dark:text-slate-200",
"bg-slate-50 hover:bg-slate-100 dark:bg-white/5 dark:hover:bg-white/10",
"outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
].join(" ")}
>
<span>{open ? "Hide details" : "See what's inside"}</span>
<ChevronIcon
className={[
"h-4 w-4 text-slate-400 transition-transform duration-300",
open ? "rotate-180" : "",
].join(" ")}
/>
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
key="panel"
id={panelId}
role="region"
aria-labelledby={btnId}
initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
animate={reduce ? { opacity: 1 } : { height: "auto", opacity: 1 }}
exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
transition={{ duration: reduce ? 0.15 : 0.4, ease: EASE }}
className="overflow-hidden"
>
<div className="cardx-fade-in pt-4">
<ul className="space-y-2.5">
{plan.highlights.map((h) => (
<li key={h} className="flex gap-2.5 text-sm text-slate-600 dark:text-slate-300">
<CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
<span>{h}</span>
</li>
))}
</ul>
<div className="mt-4 flex flex-wrap gap-2">
{plan.includes.map((inc) => (
<span
key={inc}
className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 px-2.5 py-1 text-xs font-medium text-slate-600 dark:border-white/10 dark:text-slate-300"
>
<BoxIcon className="h-3.5 w-3.5 text-indigo-500 dark:text-indigo-400" />
{inc}
</span>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<button
type="button"
className={[
"mt-5 w-full rounded-xl px-4 py-2.5 text-sm font-semibold text-white",
"bg-indigo-600 hover:bg-indigo-500 active:scale-[0.99]",
"transition-[background-color,transform] duration-150 outline-none",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
].join(" ")}
>
{plan.cta}
</button>
</div>
</div>
);
}
export default function CardExpand() {
return (
<section className="relative w-full bg-slate-50 px-5 py-20 dark:bg-slate-950 sm:px-8">
<style>{`
@keyframes cardx-fade {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.cardx-fade-in { animation: cardx-fade 0.45s cubic-bezier(0.22,1,0.36,1) both; }
@media (prefers-reduced-motion: reduce) {
.cardx-fade-in { animation: none; }
}
`}</style>
<div className="mx-auto max-w-6xl">
<header className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:border-white/10 dark:bg-slate-900 dark:text-slate-400">
Expand in place
</span>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Tap a card to see the full story
</h2>
<p className="mt-3 text-base text-slate-500 dark:text-slate-400">
Each card stays compact until you want the detail — then it grows in place, no modal, no page jump.
</p>
</header>
<div className="mt-12 grid grid-cols-1 items-start gap-6 sm:grid-cols-2 lg:grid-cols-3">
{PLANS.map((plan) => (
<ExpandCard key={plan.id} plan={plan} />
))}
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

