Accordion Soft

It is a refined, minimal UI component built for Next.js and Framer Motion. It prioritizes "Organic Interactions" by using a spring-based physics engine for the expansion and rotation effects. The layout is optimized for readability, utilizing stone-toned color palettes and generous padding to maintain a premium, architectural feel.

Live Preview
Focusing on the skeletal strength of the web through clean, semantic markup.

Dependencies

npm installframer-motionlucide-react

Code

"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown } from "lucide-react";

const data = [
  {
    title: "Architectural Integrity",
    content:
      "Focusing on the skeletal strength of the web through clean, semantic markup.",
  },
  {
    title: "Organic Interactions",
    content:
      "Moving away from rigid transitions toward fluid, physics-based motion.",
  },
];

export default function SoftAccordion({ items = data }: any) {
  const [expanded, setExpanded] = useState<number | null>(0);

  return (
    <div className="max-w-md mx-auto w-lg space-y-2">
      {items.map((item: any, i: number) => (
        <div key={i} className="overflow-hidden border-b border-stone-200">
          <button
            onClick={() => setExpanded(expanded === i ? null : i)}
            className="flex w-full items-center justify-between py-6 text-left"
          >
            <span className="text-lg font-medium text-stone-800">
              {item.title}
            </span>
            <motion.div
              animate={{ rotate: expanded === i ? 180 : 0 }}
              transition={{ type: "spring", stiffness: 300, damping: 20 }}
            >
              <ChevronDown className="w-5 h-5 text-stone-500" />
            </motion.div>
          </button>
          <AnimatePresence initial={false}>
            {expanded === i && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: "auto", opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }}
              >
                <div className="pb-6 text-stone-600 leading-relaxed">
                  {item.content}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      ))}
    </div>
  );
}

Props

PropTypeDefaultDescription
itemsAccordionItem[]dataAn array of objects representing each accordion row, including the header title and body content.