An animated, accessible FAQ accordion for React and Tailwind CSS. Smooth open and close transitions.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
<Faq className="max-w-2xl">
<FaqItem value="1">
<FaqItemHeader>
<div className="flex flex-col gap-2">
<span className="text-sm">Cool Company <span className="ml-4 text-muted-foreground">2025 - present</span></span>
<span className="text-base font-serif font-medium">Software Engineer</span>
</div>
</FaqItemHeader>
<FaqItemContent>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.</p>
</FaqItemContent>
</FaqItem>
<FaqItem value="2">
<FaqItemHeader>
<div className="flex flex-col gap-2">
<span className="text-sm">Awesome Company <span className="ml-4 text-muted-foreground">2023 - 2025</span></span>
<span className="text-base font-serif font-medium">Pizza Maker</span>
</div>
</FaqItemHeader>
<FaqItemContent>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.</p>
</FaqItemContent>
</FaqItem>
</Faq>Copy and paste the following code into your project.
"use client";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import { cn } from "@/lib/utils";
function FaqPlusIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
className={cn("size-4", className)}
viewBox="0 0 24 24"
>
<path
d="M 0,12 L 24,12"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="3"
/>
<path
d="M 12,0 L 12,24"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="3"
/>
</svg>
);
}
function FaqMinusIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
className={cn("size-4", className)}
viewBox="0 0 24 24"
>
<path
d="M 0,12 L 24,12"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="3"
/>
</svg>
);
}
interface FaqContextValue {
isOpen: (value: string) => boolean;
toggle: (value: string) => void;
}
const FaqContext = createContext<FaqContextValue | null>(null);
function useFaq() {
const ctx = useContext(FaqContext);
if (!ctx) {
throw new Error("Faq components must be used inside <Faq>");
}
return ctx;
}
interface FaqItemContextValue {
open: boolean;
toggle: () => void;
value: string;
}
const FaqItemContext = createContext<FaqItemContextValue | null>(null);
function useFaqItem() {
const ctx = useContext(FaqItemContext);
if (!ctx) {
throw new Error(
"FaqItemHeader and FaqItemContent must be used inside <FaqItem>"
);
}
return ctx;
}
function toItemSet(value: string | string[] | undefined): Set<string> {
if (value === undefined) {
return new Set();
}
return new Set(Array.isArray(value) ? value : [value]);
}
function toFaqValue(multiple: boolean, items: Set<string>): string | string[] {
const values = [...items];
if (!multiple) {
return values[0] ?? "";
}
return values;
}
type FaqProps = React.ComponentProps<"div"> & {
multiple?: boolean;
defaultValue?: string | string[];
value?: string | string[];
onValueChange?: (value: string | string[]) => void;
};
function Faq({
multiple = false,
defaultValue,
value: controlledValue,
onValueChange,
className,
children,
...props
}: FaqProps) {
const [uncontrolledItems, setUncontrolledItems] = useState<Set<string>>(() =>
toItemSet(defaultValue)
);
const isControlled = controlledValue !== undefined;
const openItems = isControlled
? toItemSet(controlledValue)
: uncontrolledItems;
const updateOpenItems = useCallback(
(next: Set<string>) => {
if (!isControlled) {
setUncontrolledItems(next);
}
onValueChange?.(toFaqValue(multiple, next));
},
[isControlled, onValueChange, multiple]
);
const toggle = useCallback(
(itemValue: string) => {
const next = new Set(openItems);
if (next.has(itemValue)) {
next.delete(itemValue);
} else if (multiple) {
next.add(itemValue);
} else {
next.clear();
next.add(itemValue);
}
updateOpenItems(next);
},
[openItems, multiple, updateOpenItems]
);
const isOpen = useCallback(
(itemValue: string) => openItems.has(itemValue),
[openItems]
);
const contextValue = useMemo(() => ({ isOpen, toggle }), [isOpen, toggle]);
return (
<FaqContext.Provider value={contextValue}>
<div
className={cn("flex flex-col", className)}
data-slot="faq"
{...props}
>
{children}
</div>
</FaqContext.Provider>
);
}
Faq.displayName = "Faq";
interface FaqItemProps {
children: ReactNode;
className?: string;
value: string;
}
function FaqItem({ value, className, children }: FaqItemProps) {
const { isOpen, toggle } = useFaq();
const open = isOpen(value);
const itemContextValue = useMemo(
() => ({
open,
toggle: () => toggle(value),
value,
}),
[value, open, toggle]
);
return (
<FaqItemContext.Provider value={itemContextValue}>
<div
className={cn("faq-item", className)}
data-slot="faq-item"
role="group"
>
{children}
</div>
</FaqItemContext.Provider>
);
}
FaqItem.displayName = "FaqItem";
type FaqItemHeaderProps = Omit<React.ComponentProps<"button">, "type"> & {
iconClassName?: string;
};
function FaqItemHeader({
className,
children,
onClick,
iconClassName,
...props
}: FaqItemHeaderProps) {
const { open, toggle } = useFaqItem();
return (
<button
aria-expanded={open}
className={cn(
"flex w-full items-center justify-between gap-4 py-6",
className
)}
onClick={(event) => {
toggle();
onClick?.(event);
}}
type="button"
{...props}
>
<div className="flex min-w-0 flex-1">{children}</div>
<span
className={cn(
"transition-transform duration-400 ease-[cubic-bezier(0.33,1.53,0.69,0.99)] data-[state=open]:rotate-360 motion-reduce:transition-none",
iconClassName
)}
data-state={open ? "open" : "closed"}
>
{open ? <FaqMinusIcon /> : <FaqPlusIcon />}
</span>
</button>
);
}
FaqItemHeader.displayName = "FaqItemHeader";
type FaqItemContentProps = React.ComponentProps<"div">;
function FaqItemContent({
className,
children,
...props
}: FaqItemContentProps) {
const { open } = useFaqItem();
return (
<div
className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-200 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=open]:duration-350 data-[state=open]:ease-out motion-reduce:transition-none"
data-state={open ? "open" : "closed"}
inert={!open}
>
<div className="overflow-hidden">
<div className={cn("flex flex-col gap-4 pb-3", className)} {...props}>
{children}
</div>
</div>
</div>
);
}
FaqItemContent.displayName = "FaqItemContent";
type FaqDividerProps = React.ComponentProps<"hr">;
function FaqDivider({ className, ...props }: FaqDividerProps) {
return (
<hr
className={cn(
"h-px w-full border-0 transition-opacity duration-200",
"bg-gray-500/20",
className
)}
data-slot="faq-divider"
{...props}
/>
);
}
FaqDivider.displayName = "FaqDivider";
export type {
FaqDividerProps,
FaqItemContentProps,
FaqItemHeaderProps,
FaqItemProps,
FaqProps,
};
export { Faq, FaqDivider, FaqItem, FaqItemContent, FaqItemHeader };A single item, closed by default.
Copy the component into your project, install the dependencies and you are ready to go.
<Faq className="w-full max-w-xl">
<FaqItem value="install">
<FaqItemHeader>
<span className="font-medium text-base">How do I install it?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Copy the component into your project, install the dependencies and you are ready to go.</p>
</FaqItemContent>
</FaqItem>
</Faq>Use defaultValue to open an item on first render.
Copy the component into your project, install the dependencies and you are ready to go.
Yes. Every part accepts a className, so you can style it with Tailwind like any other element.
Yes. When the user prefers reduced motion, all the animations are disabled.
<Faq className="w-full max-w-xl" defaultValue="customize">
<FaqItem value="install">
<FaqItemHeader>
<span className="font-medium text-base">How do I install it?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Copy the component into your project, install the dependencies and you are ready to go.</p>
</FaqItemContent>
</FaqItem>
<FaqDivider />
<FaqItem value="customize">
<FaqItemHeader>
<span className="font-medium text-base">Can I customize the styles?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Yes. Every part accepts a className, so you can style it with Tailwind like any other element.</p>
</FaqItemContent>
</FaqItem>
<FaqDivider />
<FaqItem value="reduced-motion">
<FaqItemHeader>
<span className="font-medium text-base">Does it respect reduced motion?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Yes. When the user prefers reduced motion, all the animations are disabled.</p>
</FaqItemContent>
</FaqItem>
</Faq>Set multiple to let several items stay open at the same time. With multiple, defaultValue accepts an array.
Copy the component into your project, install the dependencies and you are ready to go.
Yes. Every part accepts a className, so you can style it with Tailwind like any other element.
Yes. When the user prefers reduced motion, all the animations are disabled.
<Faq
className="w-full max-w-xl"
defaultValue={["install", "reduced-motion"]}
multiple
>
<FaqItem value="install">
<FaqItemHeader>
<span className="font-medium text-base">How do I install it?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Copy the component into your project, install the dependencies and you are ready to go.</p>
</FaqItemContent>
</FaqItem>
<FaqDivider />
<FaqItem value="customize">
<FaqItemHeader>
<span className="font-medium text-base">Can I customize the styles?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Yes. Every part accepts a className, so you can style it with Tailwind like any other element.</p>
</FaqItemContent>
</FaqItem>
<FaqDivider />
<FaqItem value="reduced-motion">
<FaqItemHeader>
<span className="font-medium text-base">Does it respect reduced motion?</span>
</FaqItemHeader>
<FaqItemContent>
<p className="text-muted-foreground">Yes. When the user prefers reduced motion, all the animations are disabled.</p>
</FaqItemContent>
</FaqItem>
</Faq>It inherits all the props of the native div component, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | The class name to be applied to the FAQ. |
| children | ReactNode | - | The content of the FAQ. |
| multiple | boolean | false | Whether to allow multiple items to be open at the same time. |
| defaultValue | string | string[] | - | The default value of the FAQ. |
| value | string | string[] | - | The value of the FAQ. |
| onValueChange | (value: string | string[]) => void | - | The callback function to be called when the value changes. |
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | The value of the FAQ item. |
| className | string | - | The class name to be applied to the FAQ item. |
| children | ReactNode | - | The content of the FAQ item. |
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | The class name to be applied to the FAQ item header. |
| children | ReactNode | - | The content of the FAQ item header. |
| onClick | (event: React.MouseEvent<HTMLButtonElement>) => void | - | The callback function to be called when the header is clicked. |
| iconClassName | string | - | The class name to be applied to the FAQ item icon. |
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | The class name to be applied to the FAQ item content. |
| children | ReactNode | - | The content of the FAQ item content. |
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | The class name to be applied to the FAQ divider. |