Bouncy Accordion
Elastic easing and smooth height animation for a playful reveal.
Mainstream Ready
01
Bouncy Accordion
A standard accordion enhanced with GSAP elastic easing for a "bouncy" feel when opening.
Installation
npx shadcn@latest add https://aftershade.pages.dev/registry/bouncy-accordion.json
Source Code
"use client";
import React, { useRef, useEffect, useState } from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import gsap from "gsap";
export function BouncyAccordion({
items,
className
}: {
items: { title: string; content: string; id: string }[];
className?: string;
}) {
return (
<AccordionPrimitive.Root
type="single"
collapsible
className={cn("w-full space-y-4", className)}
>
{items.map((item) => (
<BouncyAccordionItem key={item.id} value={item.id} title={item.title}>
{item.content}
</BouncyAccordionItem>
))}
</AccordionPrimitive.Root>
);
}
function BouncyAccordionItem({
value,
title,
children,
className
}: {
value: string;
title: string;
children: React.ReactNode;
className?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const chevronRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!contentRef.current || !innerRef.current) return;
if (isOpen) {
// Open animation with bounce
gsap.to(contentRef.current, {
height: "auto",
duration: 0.8,
ease: "elastic.out(1, 0.6)",
onStart: () => {
gsap.set(contentRef.current, { overflow: "visible" });
}
});
gsap.to(innerRef.current, {
opacity: 1,
y: 0,
scale: 1,
duration: 0.5,
delay: 0.1,
ease: "power2.out"
});
gsap.to(chevronRef.current, {
rotate: 180,
duration: 0.4,
ease: "power2.out"
});
} else {
// Close animation
gsap.to(contentRef.current, {
height: 0,
duration: 0.4,
ease: "power2.inOut",
onStart: () => {
gsap.set(contentRef.current, { overflow: "hidden" });
}
});
gsap.to(innerRef.current, {
opacity: 0,
y: -10,
scale: 0.98,
duration: 0.3,
ease: "power2.in"
});
gsap.to(chevronRef.current, {
rotate: 0,
duration: 0.4,
ease: "power2.out"
});
}
}, [isOpen]);
return (
<AccordionPrimitive.Item
value={value}
className={cn(
"group overflow-hidden rounded-3xl border border-white/5 bg-secondary-1/5 transition-colors hover:bg-secondary-1/10",
isOpen && "bg-secondary-1/10 border-white/10",
className
)}
>
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
onClick={() => setIsOpen(!isOpen)}
className="flex flex-1 items-center justify-between p-6 text-left text-lg font-semibold transition-all outline-none"
>
<span className="bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
{title}
</span>
<div ref={chevronRef} className="text-primary-1">
<ChevronDown className="h-6 w-6" />
</div>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content
className="overflow-hidden"
style={{ height: 0 }}
ref={contentRef}
>
<div ref={innerRef} className="p-6 pt-0 text-muted-foreground leading-relaxed opacity-0">
{children}
</div>
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
);
}