Accordion

Next

A collapsible content component with smooth animations, built on Radix UI primitives and enhanced with Framer Motion for fluid transitions.

Docs
moleculeuiui

Preview

Loading preview…
registry/molecule-ui/accordion.tsx
"use client"

import React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { motion } from "motion/react"

import { cn } from "@/lib/utils"

const accordionContentVariants = {
  open: {
    height: "auto",
    opacity: 1,
    filter: "blur(0px)",
  },
  closed: {
    height: 0,
    opacity: 0,
    filter: "blur(4px)",
  },
}

function Accordion({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return (
    <AccordionPrimitive.Root
      data-slot="accordion"
      className={cn("bg-primary-foreground rounded-lg border px-5", className)}
      {...props}
    />
  )
}

function AccordionItem({
  children,
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn("relative border-b last:border-b-0", className)}
      {...props}
    >
      {children}
    </AccordionPrimitive.Item>
  )
}

function AccordionTrigger({
  children,
  className,
  showIcon = true,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
  showIcon?: boolean
}) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-header"
        className="group active:text-foreground/50 focus-visible:bg-muted flex flex-1 items-start justify-between gap-4 py-4 font-semibold disabled:opacity-50"
        {...props}
      >
        {children}
        {showIcon && (
          <ChevronDown className="size-6 duration-300 group-data-[state=open]:rotate-180" />
        )}
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  const contentRef = React.useRef<HTMLDivElement>(null)
  const [isOpen, setIsOpen] = React.useState(false)

  React.useEffect(() => {
    const element = contentRef.current?.parentElement
    if (!element) return

    const observer = new MutationObserver(() => {
      const state = element.getAttribute("data-state")
      setIsOpen(state === "open")
    })

    observer.observe(element, {
      attributes: true,
      attributeFilter: ["data-state"],
    })

    // Set initial state
    const initialState = element.getAttribute("data-state")
    setIsOpen(initialState === "open")

    return () => observer.disconnect()
  }, [])

  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      forceMount
      className="overflow-hidden"
      {...props}
    >
      <motion.div
        ref={contentRef}
        animate={isOpen ? "open" : "closed"}
        initial={"closed"}
        variants={accordionContentVariants}
        transition={{
          height: {
            duration: 0.3,
            ease: "easeOut",
          },
          opacity: {
            duration: 0.2,
            delay: 0.1,
          },
          filter: {
            duration: 0.15,
            delay: 0.05,
          },
        }}
      >
        <div className={cn("text-muted-foreground pb-4", className)}>
          {children}
        </div>
      </motion.div>
    </AccordionPrimitive.Content>
  )
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

Installation

npx shadcn@latest add @moleculeui/accordion

Usage

import { Accordion } from "@/components/ui/accordion"
<Accordion />