GitHub

FAQ Spring

Install

Copy and paste the following code into your project.

"use client";
 
import { cn } from "@/lib/utils";
import { ChevronDown, Code, Heart, Sparkles } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useMemo, useState } from "react";
 
const data = [
  {
    id: "one",
    icon: <Sparkles className="size-5 text-neutral-400" />,
    title: "What is Star UI?",
    content:
      "Star UI is a powerful React animation component library that allows you to create smooth, interactive animations with minimal code. It provides a simple API for creating complex animations and transitions.",
  },
  {
    id: "two",
    icon: <Code className="size-5 text-neutral-500" />,
    title: "How do I get started?",
    content: `Head to the “Quick start” guide in the docs. If you’ve used unstyled libraries before, you’ll feel at home.`,
  },
  {
    id: "three",
    icon: <Heart className="size-5 text-neutral-500" />,
    title: "Can I use it for my project?",
    content: "Of course! Star UI is free and open source.",
  },
];
 
function FAQSpring() {
  const [value, setValue] = useState("");
  const currentIndex = useMemo(() => {
    return data.findIndex((item) => item.id === value);
  }, [value]);
 
  const getRadius = (id: string, index: number) => {
    const dataLength = data.length;
    const radius = "--radius-lg";
    const isStart = index === 0;
    const isEnd = index === dataLength - 1;
    const isExpended = id === value;
    const isPrev = index === currentIndex - 1;
    const isNext = index === currentIndex + 1;
 
    if (!value) {
      if (isStart) return `var(${radius}) var(${radius}) 0 0`;
      if (isEnd) return `0 0 var(${radius}) var(${radius})`;
      return "0 0 0 0";
    } else {
      if (isExpended || (isPrev && isStart) || (isNext && isEnd)) {
        return `var(${radius})`;
      }
      if (isNext) return `var(${radius}) var(${radius}) 0 0`;
      if (isPrev) return `0 0 var(${radius}) var(${radius}`;
    }
  };
 
  const getMargin = (id: string, index: number) => {
    const dataLength = data.length;
    const isExpended = id === value;
    const isStart = index === 0;
    const isEnd = index === dataLength - 1;
    const margin = "calc(var(--spacing) * 5)";
    if (isExpended && isEnd) {
      return `${margin} 0`;
    }
    if (isExpended && isStart) {
      return `0 ${margin}`;
    }
    if (isExpended && !isStart && !isEnd) {
      return margin;
    }
  };
 
  return (
    <div className="max-w-sm w-full">
      {data.map((item, index) => (
        <motion.div
          initial={false}
          key={item.id}
          className={cn(
            "bg-white border-x border-neutral-200 overflow-hidden transition",
            {
              "border-t":
                index === 0 || value === item.id || currentIndex + 1 === index,
              "border-b":
                index === data.length - 1 ||
                value === item.id ||
                currentIndex - 1 === index,
            }
          )}
          animate={{
            borderRadius: getRadius(item.id, index),
            marginBlock: getMargin(item.id, index),
          }}
          transition={{
            type: "spring",
          }}
        >
          <h3>
            <button
              onClick={() => {
                setValue(value === item.id ? "" : item.id);
              }}
              className="font-medium cursor-pointer px-3 py-2 w-full flex items-center justify-between gap-4"
            >
              <div className="flex items-center gap-2">
                {item.icon}
                {item.title}
              </div>
              <ChevronDown
                className={cn("size-4 transition-transform", {
                  "rotate-180": value === item.id,
                })}
              />
            </button>
          </h3>
 
          <AnimatePresence>
            {value === item.id && (
              <motion.div
                initial={{ opacity: 0, height: 0 }}
                animate={{ opacity: 1, height: "auto" }}
                exit={{ opacity: 0, height: 0 }}
                className="text-sm"
                transition={{
                  type: "spring",
                  stiffness: 100,
                  damping: 10,
                  mass: 1,
                }}
              >
                <p className="pb-3 px-3">{item.content}</p>
              </motion.div>
            )}
          </AnimatePresence>
        </motion.div>
      ))}
    </div>
  );
}
 
export { FAQSpring };

Inspired by @nitishkmrk.