
Have you ever found yourself passing props down three layers deep just to handle a simple “open/close” state?
It’s a common headache called prop drilling.
In this tutorial, we’re going to build a high-quality, reusable Accordion component using the Compound Component Pattern and React Context.
This approach keeps your code clean, flexible, and easy for other developers to read.
Why Use This Pattern?
Instead of one giant component with 10 different props, we break the accordion into logical pieces: Accordion, AccordionItem, AccordionHeader, and AccordionPanel.
This gives you:
-
Flexibility: You can reorder the header and panel or add custom styles easily.
-
Cleaner State: The parent
Accordionmanages which items are open, while the children just “consume” that information.
Step 1: Setting Up the Context
First, we need a way to share state across all parts of our accordion without passing props manually.
import React, { useState, createContext, useContext } from 'react';
const AccordionContext = createContext();
export const Accordion = ({ children, allowMultiple = false }) => {
const [openIndices, setOpenIndices] = useState([]);
const toggleItem = (index) => {
if (allowMultiple) {
// Toggle logic for multiple items open at once
setOpenIndices(prev =>
prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index]
);
} else {
// Logic for "Accordion" style (only one open)
setOpenIndices(prev => prev.includes(index) ? [] : [index]);
}
};
return (
<AccordionContext.Provider value={{ openIndices, toggleItem }}>
<div className="border rounded-md divide-y shadow-sm">{children}</div>
</AccordionContext.Provider>
);
};
Step 2: The Item Wrapper
The AccordionItem acts as a middleman.
It checks the Context to see if its specific index is currently active.
export const AccordionItem = ({ index, children }) => {
const { openIndices, toggleItem } = useContext(AccordionContext);
const isOpen = openIndices.includes(index);
return (
<div className={`accordion-item ${isOpen ? 'bg-gray-50' : ''}`}>
{/* We use React.cloneElement to pass the state down to the Header and Panel */}
{React.Children.map(children, child =>
React.cloneElement(child, { index, isOpen, toggleItem })
)}
</div>
);
};
Step 3: Header and Panel (The UI)
Now we build the parts the user actually sees. Notice the aria-expanded and role="region" attributes—these are crucial for accessibility (A11y), ensuring screen readers can navigate your component.
The Header:
export const AccordionHeader = ({ children, index, isOpen, toggleItem }) => (
<button
className="w-full flex justify-between items-center p-4 font-medium text-left transition-colors hover:bg-gray-100"
onClick={() => toggleItem(index)}
aria-expanded={isOpen}
>
{children}
<span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}>
▼
</span>
</button>
);
The Panel:
export const AccordionPanel = ({ children, isOpen }) => (
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
isOpen ? 'max-h-96 opacity-100 p-4' : 'max-h-0 opacity-0 p-0'
}`}
role="region"
>
<div className="pb-2 text-gray-600">{children}</div>
</div>
);
Step 4: Putting It All Together
The beauty of this pattern is how it looks when you actually use it.
It reads almost like HTML.
function App() {
return (
<Accordion allowMultiple={false}>
<AccordionItem index={0}>
<AccordionHeader>What is React Context?</AccordionHeader>
<AccordionPanel>It is a way to share values between components without prop drilling.</AccordionPanel>
</AccordionItem>
<AccordionItem index={1}>
<AccordionHeader>Is this accessible?</AccordionHeader>
<AccordionPanel>Yes! It uses ARIA roles and keyboard-friendly button elements.</AccordionPanel>
</AccordionItem>
</Accordion>
);
}
Key Takeaways
-
allowMultiple: By simply changing this prop on the root component, you switch between a strict accordion and a “collapsible” list. -
CSS Transitions: We used
max-heightandopacityto create a smooth slide-down effect without needing a heavy animation library. -
Encapsulation: All the logic lives inside the components, keeping your main page code clean.
Useful links below:
Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service
Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2
Best email marketing automation solution on the market! http://www.aweber.com/?373860
Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29
Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff
Buy me a coffee https://buymeacoffee.com/tyronneratcliff



