Building an Expandable Dropdown Menu in React (Two Approaches)
Expandable dropdowns (or accordions) are everywhere, FAQs, menus, sidebars, you name it. In this post, we’ll walk through two different ways to build one in React.
First, we’ll take the route a newer developer might think of right away: tracking each dropdown separately. Then we’ll level it up and make it leaner and more scalable.
Approach 1: One State Per Dropdown
When you’re starting out, it’s tempting to just keep separate pieces of state for each dropdown. It’s straightforward: if you have three dropdowns, you make three state variables.
Here’s the basic idea:
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const [isOpen3, setIsOpen3] = useState(false);
Then, each button toggles its own piece of state:
<button onClick={() => setIsOpen1(!isOpen1)}>Section 1</button>
<div className={isOpen1 ? "text-box show" : "text-box"}>
This is section 1 content.
</div>
Repeat the same pattern for Section 2 and Section 3. While this is easy to understand for small numbers of dropdowns, it gets repetitive quickly. This works fine for a fixed FAQ with just a few items, but if the number of sections changes or grows, you’ll find yourself copy-pasting a lot.
Approach 2: Single State with Mapping
A more scalable approach is to store just one piece of state that represents which dropdown is open. Instead of a separate variable for each section, we keep track of the index of the open section.
If none are open, we can store -1 as the default.
const toggleSection = (index) => {
setOpenIndex(openIndex === index ? -1 : index);
};
In our dropdown/accordian challenge we hardcoded the contents of the dropdown. However, when working with APIs, you will often receive data in JSON format. If you're coming from there, just reformat the data to match the following format:
const sections = [
{ title: "Section 1: Introduction", content: "Intro content..." },
{ title: "Section 2: Details", content: "Details content..." },
{ title: "Section 3: Conclusion", content: "Conclusion content..." }
];
return (
<div>
{sections.map((section, index) => (
<div key={index}>
<button onClick={() => toggleSection(index)}>{section.title}</button>
<div className={openIndex === index ? "text-box show" : "text-box"}>
{section.content}
</div>
</div>
))}
</div>
);
This method is much easier to scale to any number of sections while only managing a single piece of state.
Bonus: Add Smooth Opening/Closing Animations
Right now, our dropdown content just pops in and out instantly. That works, but it’s not very… delightful. Let’s give it a smooth slide effect when it opens and closes.
We can do this entirely with just a few lines of CSS!
Instead of using display: none and display: block, we’ll use max-height to animate the opening and closing. We’ll also add a transition so it slides smoothly.
.text-box {
max-height: 0;
padding: 0 8px 0 8px;
overflow: hidden;
transition: 0.3s ease;
}
.text-box.show {
padding: 8px;
max-height: 40px;
}
Now when you click a section, it slides open gracefully and collapses smoothly instead of snapping in and out. It’s a small touch, but it makes the component feel much more polished and user-friendly.