Accordion

Collapsible content sections supporting single or multiple open items. Install Accordion from the VLLNT UI registry with the shadcn CLI.

Report a bug

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.json

Source

"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.

Dependencies

  • @vllnt/ui@^0.3.0