Expandable Card
- Source
- Aceternity UI ↗
- Author
- Aceternity UI
- Licence
- Aceternity UI licence (free component)
Loading…
Clicking a list row morphs it into a full card with shared-layout animation.
Code
"use client";
import React, { useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useOutsideClick } from "@/lab/aceternity/ui/use-outside-click-a";
import { Fit } from "./_fit";
export default function ExpandableCardDemo() {
const [active, setActive] = useState<(typeof cards)[number] | boolean | null>(
null
);
const ref = useRef<HTMLDivElement>(null);
const id = useId();
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
setActive(false);
}
}
// Lab: no body scroll lock; the expanded card lives inside the stage.
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [active]);
useOutsideClick(ref, () => setActive(null));
return (
<Fit width={640} height={540}>
<div className="relative flex h-full w-full items-center">
<AnimatePresence>
{active && typeof active === "object" && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/20 h-full w-full z-10"
/>
)}
</AnimatePresence>
<AnimatePresence>
{active && typeof active === "object" ? (
<div className="absolute inset-0 grid place-items-center z-[100]">
<motion.button
key={`button-${active.title}-${id}`}
layout
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
className="flex absolute top-2 right-2 hidden items-center justify-center bg-white rounded-full h-6 w-6"
onClick={() => setActive(null)}
>
<CloseIcon />
</motion.button>
<motion.div
layoutId={`card-${active.title}-${id}`}
ref={ref}
className="w-full max-w-[500px] h-fit max-h-[94%] flex flex-col rounded-3xl bg-white dark:bg-neutral-900 sm:rounded-3xl overflow-hidden"
>
<motion.div layoutId={`image-${active.title}-${id}`}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
width={200}
height={200}
src={active.src}
alt={active.title}
className="w-full h-60 rounded-tr-lg rounded-tl-lg object-cover object-top"
/>
</motion.div>
<div>
<div className="flex justify-between items-start p-4">
<div className="">
<motion.h3
layoutId={`title-${active.title}-${id}`}
className="font-bold text-neutral-700 dark:text-neutral-200"
>
{active.title}
</motion.h3>
<motion.p
layoutId={`description-${active.description}-${id}`}
className="text-neutral-600 dark:text-neutral-400"
>
{active.description}
</motion.p>
</div>
<motion.a
layoutId={`button-${active.title}-${id}`}
href={active.ctaLink}
target="_blank"
className="px-4 py-3 text-sm rounded-full font-bold bg-green-500 text-white"
>
{active.ctaText}
</motion.a>
</div>
<div className="pt-4 relative px-4">
<motion.div
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-neutral-600 text-sm h-40 pb-10 flex flex-col items-start gap-4 overflow-auto dark:text-neutral-400 [mask:linear-gradient(to_bottom,white,white,transparent)] [scrollbar-width:none] [-ms-overflow-style:none] [-webkit-overflow-scrolling:touch]"
>
{typeof active.content === "function"
? active.content()
: active.content}
</motion.div>
</div>
</div>
</motion.div>
</div>
) : null}
</AnimatePresence>
<ul className="max-w-2xl mx-auto w-full gap-4">
{cards.map((card) => (
<motion.div
layoutId={`card-${card.title}-${id}`}
key={`card-${card.title}-${id}`}
onClick={() => setActive(card)}
className="p-4 flex flex-row justify-between items-center hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-xl cursor-pointer"
>
<div className="flex gap-4 flex-row">
<motion.div layoutId={`image-${card.title}-${id}`}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
width={100}
height={100}
src={card.src}
alt={card.title}
className="h-14 w-14 rounded-lg object-cover object-top"
/>
</motion.div>
<div className="">
<motion.h3
layoutId={`title-${card.title}-${id}`}
className="font-medium text-neutral-800 dark:text-neutral-200 text-left"
>
{card.title}
</motion.h3>
<motion.p
layoutId={`description-${card.description}-${id}`}
className="text-neutral-600 dark:text-neutral-400 text-left"
>
{card.description}
</motion.p>
</div>
</div>
<motion.button
layoutId={`button-${card.title}-${id}`}
className="px-4 py-2 text-sm rounded-full font-bold bg-gray-100 hover:bg-green-500 hover:text-white text-black"
>
{card.ctaText}
</motion.button>
</motion.div>
))}
</ul>
</div>
</Fit>
);
}
const CloseIcon = () => {
return (
<motion.svg
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-black"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</motion.svg>
);
};
const cards = [
{
description: "Lana Del Rey",
title: "Summertime Sadness",
src: "https://assets.aceternity.com/demos/lana-del-rey.jpeg",
ctaText: "Play",
ctaLink: "https://ui.aceternity.com/templates",
content: () => {
return (
<p>
Lana Del Rey, an iconic American singer-songwriter, is celebrated for
her melancholic and cinematic music style. Born Elizabeth Woolridge
Grant in New York City, she has captivated audiences worldwide with
her haunting voice and introspective lyrics. <br /> <br /> Her songs
often explore themes of tragic romance, glamour, and melancholia,
drawing inspiration from both contemporary and vintage pop culture.
With a career that has seen numerous critically acclaimed albums, Lana
Del Rey has established herself as a unique and influential figure in
the music industry, earning a dedicated fan base and numerous
accolades.
</p>
);
},
},
{
description: "Babbu Maan",
title: "Mitran Di Chhatri",
src: "https://assets.aceternity.com/demos/babbu-maan.jpeg",
ctaText: "Play",
ctaLink: "https://ui.aceternity.com/templates",
content: () => {
return (
<p>
Babu Maan, a legendary Punjabi singer, is renowned for his soulful
voice and profound lyrics that resonate deeply with his audience. Born
in the village of Khant Maanpur in Punjab, India, he has become a
cultural icon in the Punjabi music industry. <br /> <br /> His songs
often reflect the struggles and triumphs of everyday life, capturing
the essence of Punjabi culture and traditions. With a career spanning
over two decades, Babu Maan has released numerous hit albums and
singles that have garnered him a massive fan following both in India
and abroad.
</p>
);
},
},
{
description: "Metallica",
title: "For Whom The Bell Tolls",
src: "https://assets.aceternity.com/demos/metallica.jpeg",
ctaText: "Play",
ctaLink: "https://ui.aceternity.com/templates",
content: () => {
return (
<p>
Metallica, an iconic American heavy metal band, is renowned for their
powerful sound and intense performances that resonate deeply with
their audience. Formed in Los Angeles, California, they have become a
cultural icon in the heavy metal music industry. <br /> <br /> Their
songs often reflect themes of aggression, social issues, and personal
struggles, capturing the essence of the heavy metal genre. With a
career spanning over four decades, Metallica has released numerous hit
albums and singles that have garnered them a massive fan following
both in the United States and abroad.
</p>
);
},
},
{
description: "Led Zeppelin",
title: "Stairway To Heaven",
src: "https://assets.aceternity.com/demos/led-zeppelin.jpeg",
ctaText: "Play",
ctaLink: "https://ui.aceternity.com/templates",
content: () => {
return (
<p>
Led Zeppelin, a legendary British rock band, is renowned for their
innovative sound and profound impact on the music industry. Formed in
London in 1968, they have become a cultural icon in the rock music
world. <br /> <br /> Their songs often reflect a blend of blues, hard
rock, and folk music, capturing the essence of the 1970s rock era.
With a career spanning over a decade, Led Zeppelin has released
numerous hit albums and singles that have garnered them a massive fan
following both in the United Kingdom and abroad.
</p>
);
},
},
{
description: "Mustafa Zahid",
title: "Toh Phir Aao",
src: "https://assets.aceternity.com/demos/toh-phir-aao.jpeg",
ctaText: "Play",
ctaLink: "https://ui.aceternity.com/templates",
content: () => {
return (
<p>
"Aawarapan", a Bollywood movie starring Emraan Hashmi, is
renowned for its intense storyline and powerful performances. Directed
by Mohit Suri, the film has become a significant work in the Indian
film industry. <br /> <br /> The movie explores themes of love,
redemption, and sacrifice, capturing the essence of human emotions and
relationships. With a gripping narrative and memorable music,
"Aawarapan" has garnered a massive fan following both in
India and abroad, solidifying Emraan Hashmi's status as a
versatile actor.
</p>
);
},
},
];
Prompt
Build "Expandable Card", a card animation. What it does: Clicking a list row morphs it into a full card with shared-layout animation. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: the Motion library. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://ui.aceternity.com/components/expandable-card (Aceternity UI, Aceternity UI licence (free component)).