Accordion
Collapsible content sections supporting single or multiple open items. Install Accordion from the VLLNT UI registry with the shadcn CLI.
Collapsible content sections supporting single or multiple open items. Part of the content family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Accordion to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.com/r/accordion.jsonSource
"use client";
import { createContext, use, useCallback, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { cn } from "../../lib/utils";
// Context for accordion state
type AccordionContextValue = {
openItems: Set<string>;
toggleItem: (value: string) => void;
type: "multiple" | "single";
};
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordionContext(): AccordionContextValue {
const context = use(AccordionContext);
if (!context) {
throw new Error("AccordionItem must be used within an Accordion");
}
return context;
}
export type AccordionProps = {
children: ReactNode;
className?: string;
defaultValue?: string | string[];
type?: "multiple" | "single";
};
function Accordion({
children,
className,
defaultValue,
type = "single",
}: AccordionProps): React.ReactNode {
const [openItems, setOpenItems] = useState<Set<string>>(() => {
if (!defaultValue) return new Set();
if (Array.isArray(defaultValue)) return new Set(defaultValue);
return new Set([defaultValue]);
});
const toggleItem = useCallback(
(value: string) => {
setOpenItems((previous) => {
const newSet = new Set(previous);
if (newSet.has(value)) {
newSet.delete(value);
} else {
if (type === "single") {
newSet.clear();
}
newSet.add(value);
}
return newSet;
});
},
[type],
);
const contextValue = useMemo(
() => ({ openItems, toggleItem, type }),
[openItems, toggleItem, type],
);
return (
<AccordionContext.Provider value={contextValue}>
<div
className={cn(
"my-6 divide-y divide-border rounded-lg border",
className,
)}
>
{children}
</div>
</AccordionContext.Provider>
);
}
export type AccordionItemProps = {
children: ReactNode;
className?: string;
value: string;
};
function AccordionItem({
children,
className,
value,
}: AccordionItemProps): React.ReactNode {
return (
<div
className={cn(
"first:rounded-t-lg last:rounded-b-lg overflow-hidden",
className,
)}
data-value={value}
>
{children}
</div>
);
}
export type AccordionTriggerProps = {
children: ReactNode;
className?: string;
icon?: ReactNode;
value: string;
};
function AccordionTrigger({
children,
className,
icon,
value,
}: AccordionTriggerProps): React.ReactNode {
const { openItems, toggleItem } = useAccordionContext();
const isOpen = openItems.has(value);
return (
<button
aria-expanded={isOpen}
className={cn(
"w-full flex items-center justify-between p-4 text-left font-medium transition-colors",
"hover:bg-muted/50",
isOpen && "bg-muted/30",
className,
)}
onClick={() => {
toggleItem(value);
}}
type="button"
>
<span className="text-sm">{children}</span>
{icon ? (
<span
className={cn(
"size-4 flex-shrink-0 transition-transform duration-200",
isOpen && "rotate-180",
)}
>
{icon}
</span>
) : (
<svg
className={cn(
"size-4 flex-shrink-0 transition-transform duration-200",
isOpen && "rotate-180",
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="m6 9 6 6 6-6"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
)}
</button>
);
}
export type AccordionContentProps = {
children: ReactNode;
className?: string;
value: string;
};
function AccordionContent({
children,
className,
value,
}: AccordionContentProps): React.ReactNode {
const { openItems } = useAccordionContext();
const isOpen = openItems.has(value);
return (
<div
className={cn(
"overflow-hidden transition-all duration-200",
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0",
)}
>
<div
className={cn(
"p-4 pt-0 text-sm text-muted-foreground [&>p]:mb-2 [&>pre]:my-2",
className,
)}
>
{children}
</div>
</div>
);
}
// Attach sub-components
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.