AccordionNested
MD · 1950B gzip budgetAccordion supporting multiple levels of nested sections.
- Category
- Media & layout
- Budget
- md, several states or a live subscription
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureContent
Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/accordion-nested.jsonCopies the source into your project.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import React, { createContext, useCallback, useContext, useState } from "react";
interface AccordionContextValue {
expandedItems: Set<string>;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextValue>({
expandedItems: new Set(),
toggle: () => {},
});
export interface AccordionNestedProps {
children: React.ReactNode;
className?: string;
ariaLabel?: string;
allowMultiple?: boolean;
}
export function AccordionNested({
children,
className = "",
ariaLabel,
allowMultiple = false,
}: AccordionNestedProps) {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const toggle = useCallback(
(id: string) => {
setExpandedItems((prev) => {
const next = new Set(allowMultiple ? prev : []);
if (prev.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
},
[allowMultiple]
);
return (
<AccordionContext.Provider value={{ expandedItems, toggle }}>
<div
className={className}
role="region"
aria-label={ariaLabel}
>
{children}
</div>
</AccordionContext.Provider>
);
}
export interface AccordionItemProps {
id: string;
trigger: React.ReactNode;
children: React.ReactNode;
className?: string;
}
export function AccordionItem({
id,
trigger,
children,
className = "",
}: AccordionItemProps) {
const { expandedItems, toggle } = useContext(AccordionContext);
const isExpanded = expandedItems.has(id);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle(id);
}
},
[id, toggle]
);
return (
<div className={className} data-state={isExpanded ? "open" : "closed"}>
<h3>
<button
type="button"
aria-expanded={isExpanded}
aria-controls={`${id}-content`}
id={`${id}-trigger`}
onClick={() => toggle(id)}
onKeyDown={handleKeyDown}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
padding: "1rem",
background: "none",
border: "none",
cursor: "pointer",
fontWeight: 500,
textAlign: "start",
}}
>
{trigger}
<span
aria-hidden="true"
style={{
display: "inline-block",
transition: "transform 0.2s ease",
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
marginInlineStart: "0.5rem",
}}
>
▾
</span>
</button>
</h3>
<div
id={`${id}-content`}
role="region"
aria-labelledby={`${id}-trigger`}
hidden={!isExpanded}
style={{
overflow: "hidden",
transition: "max-height 0.3s ease",
maxHeight: isExpanded ? "500px" : 0,
}}
>
<div style={{ padding: "0 1rem 1rem" }}>
{children}
</div>
</div>
</div>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation