Accordion Elevated
ElevatedAccordion is a high-performance UI component that focuses on depth and physical presence. Unlike standard flat accordions, this version uses Framer Motion's layout prop to handle shared element transitions, allowing the background, scale, and box-shadow to animate in a unified, "elevated" way.
Live Preview
Dependencies
npm install
framer-motionlucide-reactCode
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus } from "lucide-react";
const data = [
{
title: "Architectural Integrity",
content:
"Focusing on the skeletal strength of the web through clean, semantic markup.",
},
{
title: "Organic Interactions",
content:
"Moving away from rigid transitions toward fluid, physics-based motion.",
},
];
export default function ElevatedAccordion({ items = data }: any) {
const [active, setActive] = useState<number | null>(null);
return (
<div className="flex flex-col w-lg gap-3 p-6 bg-stone-100/50 rounded-3xl">
{items.map((item: any, i: number) => {
const isOpen = active === i;
return (
<motion.div
key={i}
// Use layout prop to handle the container's size changes smoothly
layout
initial={false}
animate={{
backgroundColor: isOpen ? "#ffffff" : "rgba(255, 255, 255, 0)",
// Reduced scale jump for better stability
scale: isOpen ? 1.01 : 1,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
className={`rounded-2xl border ${
isOpen
? "border-stone-200 shadow-xl shadow-stone-200/50"
: "border-transparent"
} overflow-hidden`}
>
<button
onClick={() => setActive(isOpen ? null : i)}
className="w-full p-5 text-left flex justify-between items-center outline-none"
>
<span
className={`text-lg font-medium transition-colors duration-300 ${
isOpen ? "text-stone-900" : "text-stone-500"
}`}
>
{item.title}
</span>
{/* Custom indicator animation */}
<div className="relative flex items-center justify-center w-6 h-6">
<motion.div
className="absolute w-full h-[2px] bg-stone-300 rounded-full"
animate={{
rotate: isOpen ? 90 : 0,
backgroundColor: isOpen ? "#44403c" : "#d6d3d1",
}}
/>
<motion.div
className="absolute w-full h-[2px] bg-stone-300 rounded-full"
animate={{ backgroundColor: isOpen ? "#44403c" : "#d6d3d1" }}
/>
</div>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
key="content"
initial="collapsed"
animate="open"
exit="collapsed"
variants={{
open: {
height: "auto",
opacity: 1,
// Custom cubic-bezier for a "weighty" organic feel
transition: {
height: {
duration: 0.4,
ease: [0.04, 0.62, 0.23, 0.98],
},
opacity: { duration: 0.25, delay: 0.1 },
},
},
collapsed: {
height: 0,
opacity: 0,
transition: {
height: {
duration: 0.3,
ease: [0.04, 0.62, 0.23, 0.98],
},
opacity: { duration: 0.2 },
},
},
}}
>
<div className="px-5 pb-6 text-stone-600 leading-relaxed origin-top">
{item.content}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | AccordionItem[] | data | An array of objects used to generate the elevated cards, containing the header titles and descriptive content. |