Basic Accordion
Basic Accordion is a React and Tailwind CSS layout component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
layout
What it is
Basic Accordion is a React and Tailwind CSS layout component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion lucide-react
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Basic Accordion
"use client";
import { ChevronDown } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useState } from "react";
const CHEVRON_ROTATION_DEGREES = 180;
const CHEVRON_ANIMATION_DURATION = 0.2;
export interface AccordionItem {
content: React.ReactNode;
id: string | number;
title: string;
}
export interface BasicAccordionProps {
allowMultiple?: boolean;
className?: string;
defaultExpandedIds?: Array<string | number>;
items: AccordionItem[];
}
export default function BasicAccordion({
items,
allowMultiple = false,
className = "",
defaultExpandedIds = [],
}: BasicAccordionProps) {
const [expandedItems, setExpandedItems] =
useState<Array<string | number>>(defaultExpandedIds);
const shouldReduceMotion = useReducedMotion();
const toggleItem = (id: string | number) => {
if (expandedItems.includes(id)) {
setExpandedItems(expandedItems.filter((item) => item !== id));
} else if (allowMultiple) {
setExpandedItems([...expandedItems, id]);
} else {
setExpandedItems([id]);
}
};
return (
<div
className={`flex w-full flex-col divide-y divide-border overflow-hidden rounded-lg border ${className}`}
>
{items.map((item) => {
const isExpanded = expandedItems.includes(item.id);
return (
<div className="overflow-hidden" key={item.id}>
<button
aria-controls={`accordion-content-${item.id}`}
aria-expanded={isExpanded}
className="flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-2 bg-background px-4 py-3 text-left transition-colors hover:bg-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
id={`accordion-header-${item.id}`}
onClick={() => toggleItem(item.id)}
type="button"
>
<h3 className="font-medium">{item.title}</h3>
<motion.div
animate={{ rotate: isExpanded ? CHEVRON_ROTATION_DEGREES : 0 }}
className="shrink-0"
transition={{
duration: shouldReduceMotion ? 0 : CHEVRON_ANIMATION_DURATION,
}}
>
<ChevronDown className="h-5 w-5" />
</motion.div>
</button>
{/* Content stays mounted (height-animated, not unmounted) so the
accordion keeps a stable width whether open or closed. `inert`
removes collapsed content from tab order and the a11y tree. */}
<motion.div
animate={{
height: isExpanded ? "auto" : 0,
opacity: isExpanded ? 1 : 0,
}}
aria-labelledby={`accordion-header-${item.id}`}
className="overflow-hidden"
id={`accordion-content-${item.id}`}
inert={!isExpanded}
initial={false}
role="region"
transition={
shouldReduceMotion
? { duration: 0 }
: {
height: {
damping: 40,
duration: 0.25,
stiffness: 500,
type: "spring" as const,
},
opacity: { duration: 0.2 },
}
}
>
<div className="border-t bg-background px-4 py-3">
{item.content}
</div>
</motion.div>
</div>
);
})}
</div>
);
}