Frastra Logo

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

Default

'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
export function DefaultAccordion({ items = [], allowMultiple = true }) {
const [openItems, setOpenItems] = useState(new Set())
const toggleItem = (index) => {
setOpenItems((prev) => {
const newSet = new Set(prev)
const isOpen = newSet.has(index)
if (allowMultiple) {
isOpen ? newSet.delete(index) : newSet.add(index)
} else {
newSet.clear()
if (!isOpen) newSet.add(index)
}
return newSet
})
}
return (
<div className='w-full max-w-md'>
{items.map((item, index) => {
const isOpen = openItems.has(index)
const contentId = `accordion-content-${index}`
const buttonId = `accordion-button-${index}`
return (
<div key={index} className='border-b border-white/10 last:border-b-0'>
<button
onClick={() => toggleItem(index)}
aria-expanded={isOpen}
aria-controls={contentId}
id={buttonId}
className='w-full flex items-center justify-between py-4 text-left'
>
<span className='text-sm font-medium text-white'>
{item.question}
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDownIcon className='w-4 h-4 text-white/70' />
</motion.div>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
id={contentId}
role="region"
aria-labelledby={buttonId}
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className='overflow-hidden'
>
<div className='pb-4'>
<p className='text-sm text-white/70'>{item.answer}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
)
}

Single Item

"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
export function SingleAccordion({ items = [] }) {
const [openItem, setOpenItem] = useState(null);
const toggleItem = (index) => {
setOpenItem((prev) => (prev === index ? null : index));
};
return (
<div className="w-full max-w-md">
{items.map((item, index) => {
const isOpen = openItem === index;
const contentId = `accordion-content-${index}`;
const buttonId = `accordion-button-${index}`;
return (
<div key={index} className="border-b border-white/10 last:border-b-0">
<button
onClick={() => toggleItem(index)}
aria-expanded={isOpen}
aria-controls={contentId}
id={buttonId}
className="w-full flex items-center justify-between py-4 text-left"
>
<span className="text-sm font-medium text-white">
{item.question}
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDownIcon className="w-4 h-4 text-white/70" />
</motion.div>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
id={contentId}
role="region"
aria-labelledby={buttonId}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="pb-4">
<p className="text-sm text-white/70">{item.answer}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
);
}

Borderless

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
export function BorderlessAccordion({ items = [], allowMultiple = true }) {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = (index) => {
setOpenItems((prev) => {
const newSet = new Set(prev);
const isOpen = newSet.has(index);
if (allowMultiple) {
isOpen ? newSet.delete(index) : newSet.add(index);
} else {
newSet.clear();
if (!isOpen) newSet.add(index);
}
return newSet;
});
};
return (
<div className="w-full max-w-md space-y-2">
{items.map((item, index) => {
const isOpen = openItems.has(index);
const contentId = `accordion-content-${index}`;
const buttonId = `accordion-button-${index}`;
return (
<div key={index} className="bg-white/5 rounded-md p-4">
<button
onClick={() => toggleItem(index)}
aria-expanded={isOpen}
aria-controls={contentId}
id={buttonId}
className="w-full flex items-center justify-between text-left"
>
<span className="text-sm font-medium text-white">
{item.question}
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDownIcon className="w-4 h-4 text-white/70" />
</motion.div>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
id={contentId}
role="region"
aria-labelledby={buttonId}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="pt-3">
<p className="text-sm text-white/70">{item.answer}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
);
}
© 2025 Frastra UI. Released under the MIT License.
Designed and developed by emirkrhan