All components
Accordion
accordionsUi-Layouts component.
responsive · 520px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/accordion.jsonUsage
"use client";
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionPanel,
} from "@/registry/ui-layouts/accordion";
export default function Demo() {
return (
<div className="w-full max-w-lg mx-auto py-10 px-4">
<Accordion defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionHeader>What is a UI component?</AccordionHeader>
<AccordionPanel>
A UI component is a modular, reusable element that serves a specific function within a
graphical user interface — buttons, inputs, dropdowns, sliders, and more.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionHeader>Why are UI components important?</AccordionHeader>
<AccordionPanel>
Components promote consistency, efficiency, and scalability. They let developers reuse
code and maintain a cohesive look and feel across the entire application.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionHeader>Key characteristics of great components?</AccordionHeader>
<AccordionPanel>
Well-designed components are modular, customizable, and accessible. They have clear
functionality and adapt easily to any design language.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-4">
<AccordionHeader>How do components improve UX?</AccordionHeader>
<AccordionPanel>
Familiar, consistent interactions make navigation intuitive. Recognizable patterns reduce
cognitive load and help users accomplish tasks faster.
</AccordionPanel>
</AccordionItem>
</Accordion>
</div>
);
}Component source
'use client';
import { cn } from '@/lib/utils';
import { ChevronDown } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import React, { type ReactNode, useCallback } from 'react';
/**
* Interface for AccordionContext values
*/
interface AccordionContextType {
/**
* Whether the accordion item is active
*/
isActive?: boolean;
/**
* The value of the accordion item
*/
value?: string;
/**
* Function to change the active index
*/
onChangeIndex?: (value: string) => void;
}
/**
* Context for accordion components
*/
const AccordionContext = React.createContext<AccordionContextType>({});
/**
* Hook to use the accordion context
*/
const useAccordion = () => React.useContext(AccordionContext);
/**
* Container component for accordion items
*/
export function AccordionContainer({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn('grid grid-cols-2 gap-1', className)}>{children}</div>;
}
/**
* Wrapper component for accordion items
*/
export function AccordionWrapper({ children }: { children: ReactNode }) {
return <div>{children}</div>;
}
/**
* Interface for Accordion props
*/
interface AccordionProps {
/**
* Children components
*/
children: ReactNode;
/**
* Whether multiple items can be active at the same time
*/
multiple?: boolean;
/**
* Default active index
*/
defaultValue?: string | string[];
}
/**
* Accordion component
*/
export function Accordion({ children, multiple, defaultValue }: AccordionProps) {
/**
* State for active index
*/
const [activeIndex, setActiveIndex] = React.useState<string | string[] | null>(
multiple ? (Array.isArray(defaultValue) ? defaultValue : []) : defaultValue || null
);
/**
* Function to change the active index
*/
const onChangeIndex = useCallback(
(value: string) => {
setActiveIndex((currentActiveIndex) => {
if (!multiple) {
return value === currentActiveIndex ? null : value;
}
if (Array.isArray(currentActiveIndex)) {
if (currentActiveIndex.includes(value)) {
return currentActiveIndex.filter((i) => i !== value);
}
return [...currentActiveIndex, value];
}
return [value];
});
},
[multiple]
);
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return null;
const childProps = child.props as { value: string };
const value = childProps.value;
const isActive = multiple
? Array.isArray(activeIndex) && activeIndex.includes(value)
: activeIndex === value;
return (
<AccordionContext.Provider value={{ isActive, value, onChangeIndex }}>
{child}
</AccordionContext.Provider>
);
});
}
/**
* Interface for AccordionItem props
*/
interface AccordionItemProps {
/**
* Children components
*/
children: ReactNode;
/**
* Value of the accordion item
*/
value: string;
className?: string;
}
/**
* Accordion item component
*/
export function AccordionItem({ children, value, className }: AccordionItemProps) {
const { isActive } = useAccordion();
return (
<div
data-active={isActive || undefined}
className={cn(
'rounded-lg overflow-hidden mb-2 group border border-neutral-200 dark:border-neutral-800',
className
)}
>
{children}
</div>
);
}
/**
* Interface for AccordionHeader props
*/
interface AccordionHeaderProps {
/**
* Children components
*/
children: ReactNode;
/**
* Icon component
*/
customIcon?: boolean;
className?: string;
}
/**
* Accordion header component
*/
export function AccordionHeader({ children, customIcon, className }: AccordionHeaderProps) {
const { isActive, value, onChangeIndex } = useAccordion();
const handleClick = useCallback(() => {
if (value && onChangeIndex) {
onChangeIndex(value);
}
}, [onChangeIndex, value]);
return (
<motion.button
type='button'
data-active={isActive || undefined}
aria-expanded={isActive}
className={cn(
'p-4 cursor-pointer w-full transition-all font-semibold text-neutral-500 dark:data-active:text-neutral-200 data-active:text-neutral-800 dark:data-active:bg-neutral-800 data-active:bg-neutral-200 hover:bg-neutral-100 hover:text-black flex justify-between gap-2 items-center text-left',
className
)}
onClick={handleClick}
>
{children}
{!customIcon && (
<ChevronDown
className={cn(
'transition-transform shrink-0 text-neutral-500 dark:text-neutral-400',
isActive ? 'rotate-180' : 'rotate-0'
)}
aria-hidden='true'
/>
)}
</motion.button>
);
}
/**
* Interface for AccordionPanel props
*/
interface AccordionPanelProps {
/**
* Children components
*/
children: ReactNode;
/**
* className
*/
className?: string;
/**
* article className
*/
articleClassName?: string;
}
/**
* Accordion panel component
*/
export function AccordionPanel({ children, className, articleClassName }: AccordionPanelProps) {
const { isActive, value } = useAccordion();
return (
<AnimatePresence initial={true}>
{isActive && (
<motion.div
data-active={isActive || undefined}
role='region'
id={`accordion-panel-${value}`}
aria-labelledby={`accordion-header-${value}`}
initial={{ height: 0, overflow: 'hidden' }}
animate={{ height: 'auto', overflow: 'hidden' }}
exit={{ height: 0 }}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
className={cn(
'bg-neutral-100 dark:bg-neutral-900 px-2 data-active:bg-neutral-200 dark:data-active:bg-neutral-800 text-black dark:text-white',
className
)}
>
<motion.div
initial={{ clipPath: 'polygon(0 0, 100% 0, 100% 0, 0 0)' }}
animate={{ clipPath: 'polygon(0 0, 100% 0, 100% 100%, 0% 100%)' }}
exit={{
clipPath: 'polygon(0 0, 100% 0, 100% 0, 0 0)',
}}
transition={{
type: 'spring',
duration: 0.4,
bounce: 0,
}}
className={cn('px-3 bg-transparent pb-4 space-y-2', articleClassName)}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}Source: Ui-Layouts