logo

Nurav UI

Community
Components

Accordion

Highly interactive, smooth-animating vertical menus for clean content organization.Copy Markdown

Preview

The Accordion component comes with five distinct visual variants to match your design system.

Features

Audible Feedback

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.

Variants

Explore several pre-built visual variants.

Variant V1 (Modern Accent)

A sophisticated design featuring a floating accent border and smooth spring animations. Perfect for modern, clean interfaces.

Loading...

Variant V2 (Glassmorphism)

High-end visual depth using backdrop-blur, subtle gradients, and an animated glow effect that tracks the open state.

Loading...

Variant V3 (Minimalist Edge)

Sharp lines and a reactive left-border accent. Designed for high-density information with a brutalist yet refined aesthetic.

Loading...

Variant V4 (Gradient Fade)

A premium look featuring a right-to-left customizable color gradient and dynamic arrow icons.

Loading...

Variant V5 (Soft Outlined)

A clean, soft-outlined variant with minimalist interaction feedback and angle arrows.

Loading...

Installation

Automatic (CLI)

The fastest way to get started is using the shadcn CLI.

Before using the @nurav-ui alias, add this to your components.json:

components.json
{
  "registries": {
    "@nurav-ui": "https://nurav-ui.vercel.app/r"
  }
}

Or skip alias setup entirely and use the direct URL below.

Terminal
npx shadcn@latest add @nurav-ui/accordion

Alternatively, install via direct URL (no components.json changes needed):

Terminal
npx shadcn@latest add https://nurav-ui.vercel.app/r/accordion.json

Manual Setup

If you prefer manual configuration, follow these steps:

Install Dependencies

Install the necessary animation and utility libraries:

Terminal
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:

Accordion.tsx
'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.

Usage

Multiple Items Open

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} />

Default Value

You can specify an item to be open on initial render.

<Accordion defaultValue="item-1" />

Props

Accordion Props

PropTypeDefaultDescription
items
AccordionItem[]defaultItemsArray of items with `value`, `title`, and `content`.
variant
'v1' | 'v2' | 'v3' | 'v4' | 'v5''v1'Visual style variant for the accordion.
accentColor
stringundefinedCustomizable color for the gradient (v4) or accent (v5).
defaultValue
stringundefinedThe value of the item that should be open by default.
collapsible
booleantrueIf true, only one item can be open at a time.
disableSound
booleanfalseWhen true, disables the audible click feedback for this instance.
className
stringundefinedAdditional CSS classes for the container.

AccordionItem

PropTypeDefaultDescription
valuerequired
string—Unique identifier for the item.
titlerequired
ReactNode—The header text or component for the accordion item.
contentrequired
ReactNode—The expandable content for the accordion item.

Built by Varun

Last Updated:February 21, 2026

On this page