Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
Demo
Content A
Dependencies
lucide-reactclsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import { useState, createContext, useContext, useCallback } from "react";
import { cn } from "@/lib/utils";
import { ChevronDown } from "lucide-react";
interface AccordionContextValue {
expanded: string | null;
setExpanded: (value: string | null) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
export interface AccordionProps {
defaultValue?: string;
type?: "single" | "multiple";
children?: React.ReactNode;
className?: string;
}
export function Accordion({
defaultValue,
type = "single",
children,
className,
}: AccordionProps) {
const [expanded, setExpanded] = useState<string | null>(defaultValue ?? null);
return (
<AccordionContext.Provider value={{ expanded, setExpanded }}>
<div className={cn("divide-y divide-gray-200", className)}>
{children}
</div>
</AccordionContext.Provider>
);
}
export function AccordionItem({
value,
children,
className,
}: {
value: string;
children?: React.ReactNode;
className?: string;
}) {
return (
<div className={cn("py-1", className)} data-value={value}>
{children}
</div>
);
}
export function AccordionTrigger({
value,
children,
className,
}: {
value: string;
children?: React.ReactNode;
className?: string;
}) {
const ctx = useContext(AccordionContext);
const isExpanded = ctx?.expanded === value;
const handleClick = useCallback(() => {
ctx?.setExpanded(isExpanded ? null : value);
}, [ctx, isExpanded, value]);
return (
<button
onClick={handleClick}
className={cn(
"flex w-full items-center justify-between py-3 text-sm font-medium text-gray-900 transition-colors hover:text-gray-600",
className,
)}
>
{children}
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-gray-500 transition-transform duration-200",
isExpanded && "rotate-180",
)}
/>
</button>
);
}
export function AccordionContent({
value,
children,
className,
}: {
value: string;
children?: React.ReactNode;
className?: string;
}) {
const ctx = useContext(AccordionContext);
if (ctx?.expanded !== value) return null;
return (
<div className={cn("pb-3 text-sm text-gray-600", className)}>
{children}
</div>
);
}
CLI
npx genui add accordion