Fluid Accordion
An expandable list with smooth, physics-based height animations.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add fluid-accordionSource Code
fluid-accordion.tsx
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Plus, Minus, ChevronDown, Zap, Shield, Globe, Cpu, ArrowUpRight } from 'lucide-react';
interface AccordionItemProps {
title: string;
content: string;
icon: React.ElementType;
isOpen: boolean;
onClick: () => void;
index: number;
}
const AccordionItem = ({ title, content, icon: Icon, isOpen, onClick, index }: AccordionItemProps) => {
return (
<motion.div
initial={false}
className={`group mb-4 overflow-hidden rounded-xl border relative ${
isOpen
? 'bg-neutral-900/5 dark:bg-white/5 border-neutral-200 dark:border-white/10'
: 'bg-white dark:bg-neutral-900 border-neutral-200 dark:border-white/5 hover:border-neutral-300 dark:hover:border-white/10'
} backdrop-blur-md transition-all duration-500`}
>
{/* Shimmer Effect */}
<div className="absolute inset-0 -translate-x-full group-hover:animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/5 to-transparent pointer-events-none" />
<motion.button
initial={false}
onClick={onClick}
className="flex w-full items-center justify-between p-6 text-left relative z-10"
>
<div className="flex items-center gap-5">
<div className={`relative p-3 rounded-xl transition-all duration-500 ${
isOpen
? 'bg-neutral-900 dark:bg-white text-white dark:text-black shadow-lg scale-110'
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400 group-hover:scale-105'
}`}>
<Icon size={22} strokeWidth={1.5} />
</div>
<div>
<span className={`block text-lg font-medium tracking-tight transition-colors duration-300 ${
isOpen ? 'text-neutral-900 dark:text-white' : 'text-neutral-600 dark:text-neutral-400'
}`}>
{title}
</span>
</div>
</div>
<div className={`relative w-8 h-8 flex items-center justify-center rounded-full border transition-all duration-500 ${
isOpen
? 'border-neutral-900 dark:border-white text-neutral-900 dark:text-white rotate-180'
: 'border-neutral-200 dark:border-white/10 text-neutral-400 group-hover:border-neutral-300 dark:group-hover:border-white/20'
}`}>
<ChevronDown size={16} />
</div>
</motion.button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.section
key="content"
initial="collapsed"
animate="open"
exit="collapsed"
variants={{
open: { opacity: 1, height: "auto" },
collapsed: { opacity: 0, height: 0 }
}}
transition={{ duration: 0.4, ease: [0.04, 0.62, 0.23, 0.98] }}
>
<motion.div
variants={{ collapsed: { y: -10, opacity: 0 }, open: { y: 0, opacity: 1 } }}
transition={{ duration: 0.3, delay: 0.1 }}
className="px-6 pb-8 pl-[5.5rem] pr-12 text-neutral-500 dark:text-neutral-400 leading-relaxed text-[15px]"
>
<p>{content}</p>
</motion.div>
</motion.section>
)}
</AnimatePresence>
</motion.div>
);
};
export const FluidAccordion = () => {
const [openIndex, setOpenIndex] = useState<number | null>(0);
const items = [
{
icon: Zap,
title: "Instant Performance",
content: "Powered by Framer Motion's layout animations, every interaction feels immediate and fluid. No jank, no layout shifts, just pure 60fps buttery smoothness."
},
{
icon: Shield,
title: "Accessible by Default",
content: "Built with semantic HTML and proper ARIA attributes. Keyboard navigation works out of the box, ensuring your UI is usable by everyone."
},
{
icon: Globe,
title: "Global Theming",
content: "Seamlessly adapts to your application's design system. Whether you're using Tailwind, CSS modules, or styled-components, customization is a breeze."
},
{
icon: Cpu,
title: "Hardware Accelerated",
content: "Animations are offloaded to the GPU where possible, keeping your main thread free for complex logic and ensuring battery-friendly performance."
}
];
return (
<div className="w-full min-h-[600px] flex items-center justify-center bg-neutral-50 dark:bg-neutral-950 p-8 transition-colors duration-500 relative">
<div className="absolute inset-0 bg-grid-black/[0.02] dark:bg-grid-white/[0.02] bg-[size:20px_20px]" />
<div className="w-full max-w-2xl relative z-10">
<div className="mb-10 pl-2">
<h2 className="text-3xl font-light text-neutral-900 dark:text-white tracking-tight">Velocity <span className="font-bold">Features</span></h2>
<p className="text-neutral-500 dark:text-neutral-400 mt-2 text-lg font-light">Discover what makes it unique</p>
</div>
<div className="space-y-3">
{items.map((item, index) => (
<AccordionItem
key={index}
index={index}
title={item.title}
content={item.content}
icon={item.icon}
isOpen={openIndex === index}
onClick={() => setOpenIndex(openIndex === index ? null : index)}
/>
))}
</div>
</div>
</div>
);
};
Dependencies
framer-motion: latestlucide-react: latest

