Components
Accordion Highly interactive, smooth-animating vertical menus for clean content organization. Copy Markdown
The Accordion component comes with five distinct visual variants to match your design system.
By default, the accordion provides subtle audible feedback when expanding or collapsing items. You can control this behavior globally using the Sound Toggle component or disable it per-instance via the disableSound prop.
Explore several pre-built visual variants.
A sophisticated design featuring a floating accent border and smooth spring animations. Perfect for modern, clean interfaces.
High-end visual depth using backdrop-blur, subtle gradients, and an animated glow effect that tracks the open state.
Sharp lines and a reactive left-border accent. Designed for high-density information with a brutalist yet refined aesthetic.
A premium look featuring a right-to-left customizable color gradient and dynamic arrow icons.
A clean, soft-outlined variant with minimalist interaction feedback and angle arrows.
The fastest way to get started is using the shadcn CLI.
Before using the @nurav-ui alias , add this to your components.json:
{
"registries" : {
"@nurav-ui" : "https://nurav-ui.vercel.app/r"
}
} Or skip alias setup entirely and use the direct URL below.
npx shadcn@latest add @nurav-ui/accordion
Alternatively , install via direct URL (no components.json changes needed):
npx shadcn@latest add https://nurav-ui.vercel.app/r/accordion.json
If you prefer manual configuration, follow these steps:
Install Dependencies Install the necessary animation and utility libraries:
npm install motion lucide-react clsx tailwind-merge Create/Customize Component Copy the following code into your project (e.g., components/nurav-ui/Accordion.tsx). You can modify this code to create your own custom accordion designs:
'use client' ;
import { useState } from 'react' ;
import { motion, AnimatePresence } from 'motion/react' ;
import { ChevronDown, ChevronUp } from 'lucide-react' ;
import { useSound } from "@/hooks/use-sound" ;
interface AccordionItem {
value : string ;
title : React . ReactNode ;
content : React . ReactNode ;
}
interface AccordionProps {
items ?: AccordionItem [];
defaultValue ?: string ;
collapsible ?: boolean ;
className ?: string ;
variant ?: 'v1' | 'v2' | 'v3' | 'v4' | 'v5' ;
disableSound ?: boolean ;
accentColor ?: string ;
}
export const Accordion = ({
items,
defaultValue,
collapsible = true ,
className,
variant = "v1" ,
disableSound = false ,
accentColor,
} : AccordionProps ) => {
const { mouseClick } = useSound ();
const [ openItems , setOpenItems ] = useState < string []>(
defaultValue ? [defaultValue] : []
);
const toggleItem = ( value : string ) => {
if ( ! disableSound) mouseClick ();
setOpenItems (( prev ) => {
if (collapsible) {
return prev. includes (value) ? [] : [value];
}
return prev. includes (value)
? prev. filter (( v ) => v !== value)
: [ ... prev, value];
});
};
return (
< div className = { cn ( "w-full mx-auto flex flex-col space-y-4" , className)}>
{items?. map (( item ) => {
const isOpen = openItems. includes (item.value);
return (
< div
key = {item.value}
className = { cn (
"relative overflow-hidden transition-all duration-500" ,
"group/item border border-foreground/8 backdrop-blur-md" ,
"hover:border-foreground/20" ,
isOpen && "border-foreground/20 shadow-lg shadow-foreground/5" ,
variant === 'v1' && "rounded-2xl bg-linear-to-br from-background via-background/95 to-background/90" ,
variant === 'v2' && "rounded-3xl bg-white/5 dark:bg-zinc-950/50 border-white/10 dark:border-zinc-800/50" ,
variant === 'v3' && "bg-background border-l-4 border-l-transparent transition-all" ,
variant === 'v3' && isOpen && "border-l-primary" ,
variant === 'v4' && "rounded-xl border-none bg-zinc-100/5 dark:bg-zinc-900/20 backdrop-blur-sm" ,
variant === 'v5' && "rounded-lg border border-foreground/5 bg-transparent hover:bg-foreground/2"
)}
>
{ /* Right-to-Left Gradient for v4 */ }
{variant === 'v4' && (
< div
className = "pointer-events-none absolute inset-0 z-0 transition-opacity duration-500"
style = {{
background: `linear-gradient(to left, ${ accentColor || "var(--primary)"}, transparent)` ,
opacity: isOpen ? 0.2 : 0.05 ,
}}
/>
)}
{ /* Accent Border / Highlight */ }
{variant !== 'v3' && variant !== 'v4' && variant !== 'v5' && (
< motion.div
initial = { false }
animate = {{
opacity: isOpen ? 1 : 0 ,
scaleX: isOpen ? 1 : 0.8 ,
}}
className = { cn (
"absolute top-0 left-0 right-0 h-[2px] bg-linear-to-r from-transparent via-primary/50 to-transparent" ,
variant === 'v2' && "via-accent/50"
)}
/>
)}
< button
onClick = {() => toggleItem (item.value)}
className = { cn (
"group flex w-full items-center justify-between px-6 py-5 text-left" ,
"transition-all duration-300 focus:outline-none" ,
isOpen && "pb-3"
)}
aria-expanded = {isOpen}
>
< span className = { cn (
"text-lg font-semibold tracking-tight transition-colors duration-300" ,
isOpen ? "text-foreground" : "text-foreground/70" ,
"group-hover:text-foreground"
)}>
{item.title}
</ span >
< div className = { cn (
"relative flex h-8 w-8 items-center justify-center rounded-full transition-all duration-500" ,
(variant === 'v1' || variant === 'v2' || variant === 'v3' ) && "border border-foreground/10 bg-foreground/5" ,
(variant === 'v1' || variant === 'v2' || variant === 'v3' ) && isOpen && "rotate-180 border-primary/20 bg-primary/10 text-primary" ,
(variant === 'v4' || variant === 'v5' ) && "bg-transparent text-foreground/50 group-hover:text-foreground" ,
(variant === 'v4' || variant === 'v5' ) && isOpen && "text-primary"
)}>
{variant === 'v4' || variant === 'v5' ? (
isOpen ? < ChevronUp className = "h-5 w-5" /> : < ChevronDown className = "h-5 w-5" />
) : (
< ChevronDown className = { cn (
"h-4 w-4 transition-transform duration-500" ,
isOpen ? "opacity-100" : "opacity-40"
)} />
)}
</ div >
</ button >
< AnimatePresence initial = { false }>
{isOpen && (
< motion.div
key = "content"
initial = {{ height: 0 , opacity: 0 , scale: 0.98 }}
animate = {{ height: "auto" , opacity: 1 , scale: 1 }}
exit = {{ height: 0 , opacity: 0 , scale: 0.98 }}
transition = {{
height: { duration: 0.5 , ease: [ 0.16 , 1 , 0.3 , 1 ] },
opacity: { duration: 0.3 , delay: 0.1 },
scale: { duration: 0.4 , ease: [ 0.16 , 1 , 0.3 , 1 ] }
}}
className = "overflow-hidden"
>
< div className = { cn (
"px-6 pb-6 text-muted-foreground leading-relaxed" ,
"transition-all duration-500"
)}>
< motion.div
initial = {{ y: 5 , opacity: 0 }}
animate = {{ y: 0 , opacity: 1 }}
transition = {{ delay: 0.2 , duration: 0.4 }}
>
{item.content}
</ motion.div >
</ div >
</ motion.div >
)}
</ AnimatePresence >
</ div >
);
})}
</ div >
);
};
This component uses motion/react for smooth layout transitions and height animations.
By default, the accordion allows multiple items to be open at once. If you want to limit it to one, use the collapsible prop.
< Accordion collapsible = { true } />
You can specify an item to be open on initial render.
< Accordion defaultValue = "item-1" />
Last Updated: February 21, 2026