The "Why Is This Thing Jumping Around?" Problem ๐
You've seen it happen. You build a simple show/hide toggle, open it in the browser, and the content just snaps into existence like it teleported. No grace. No style. Just โ bam. There it is.
That's the accordion without animation. And honestly? It looks like a website from 2009.
In this post, we're going to fix that. We'll build a proper, animated accordion using Tailwind CSS that works in pure HTML, React, and Next.js โ with clean orange and black styling, smooth transitions, and the kind of polish that makes your UI feel alive.
Whether you're building an FAQ section, a settings panel, a product detail page, or a mobile nav โ the accordion is one of those components you'll use constantly. So let's build it right, once and for all.
What Is an Accordion, Anyway? ๐ค
Think of an accordion like a filing cabinet drawer. You pull one open to see what's inside, and the rest stay closed. You don't dump everything on the desk at once โ that would be chaos.
In web UI terms, an accordion is a collapsible content component. It has a header (the thing you click) and a body (the content that shows or hides). When you click the header, the body expands. Click it again โ it collapses.
Simple concept. But the execution โ especially the animation โ is where most developers either nail it or break their layout trying to do it with height: auto.
Here's a real-world example: Every FAQ section you've ever seen on a product landing page? That's an accordion. The "Show more filters" toggle on e-commerce sites? Accordion. The expandable sidebar in a dashboard? Also an accordion.
It's everywhere. And once you understand how to build it properly, it's as easy as unlocking your phone once you know the password.
Why This Matters (More Than You Think)
Here's a question worth asking: Why not just show all the content at once?
Valid thought. But here's the thing โ users don't want to read everything. They want to find what they need, fast. Accordions reduce cognitive load, keep the page clean, and let users control what they see.
From a UI/UX perspective:
- Pages feel less overwhelming
- Information is organized logically
- Mobile users love it (less scrolling = less thumb fatigue)
From a developer perspective, a poorly built accordion with janky transitions:
- Breaks layout on dynamic content
- Causes layout reflow (which tanks performance)
- Looks unprofessional, even if the rest of your site is beautiful
That last point stings because it's true. One bad transition can make a polished site feel unfinished.
Benefits of a Well-Built Animated Accordion
- โ Cleaner UI โ Only shows relevant content, keeps pages scannable
- โ Better UX โ Users feel in control when they can expand what they need
- โ Mobile-Friendly โ Works great on small screens without endless scrolling
- โ Reusable โ Build it once, use it for FAQs, filters, nav menus, settings, docs
- โ
Accessible โ With proper HTML tags (
<details>,<summary>, ARIA attributes), screen readers handle it natively - โ SEO-Friendly โ Content inside the accordion is crawlable by search engines
- โ Performance-Conscious โ CSS transitions are GPU-accelerated, meaning smooth animations without JavaScript jank
Real-life use cases:
- ๐ Product page FAQs
- ๐ง Settings or preferences panels
- ๐ Documentation with collapsible sections
- ๐ฑ Mobile navigation menus
- ๐งพ Invoice line item breakdowns
The Code: Building the Accordion (Three Ways)
Let's get into the actual implementation. We'll cover all three: pure HTML with <details>, Tailwind + vanilla JS, and React/Next.js component.
Option 1: Pure HTML with <details> and <summary> (The Semantic Way)
This is the most underrated approach. Native HTML, zero JavaScript, accessible by default.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tailwind Accordion</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
details > div {
overflow: hidden;
transition: max-height 0.4s ease, opacity 0.4s ease;
max-height: 0;
opacity: 0;
}
details[open] > div {
max-height: 500px;
opacity: 1;
}
</style>
</head>
<body class="bg-[#0a0a0a] min-h-screen flex items-center justify-center p-6">
<div class="w-full max-w-2xl space-y-3">
<!-- Accordion Item -->
<details class="group bg-[#1a1a1a] border border-[#fd6e4e]/30 rounded-xl overflow-hidden">
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer text-white font-medium text-base list-none select-none hover:bg-[#fd6e4e]/10 transition-colors duration-200">
What is Tailwind CSS?
<span class="text-[#fd6e4e] transition-transform duration-300 group-open:rotate-180">
โผ
</span>
</summary>
<div>
<p class="px-5 py-4 text-gray-400 text-sm leading-relaxed">
Tailwind CSS is a utility-first CSS framework that lets you build custom designs directly in your HTML without writing custom CSS. It's fast, flexible, and loved by developers worldwide.
</p>
</div>
</details>
<!-- Accordion Item -->
<details class="group bg-[#1a1a1a] border border-[#fd6e4e]/30 rounded-xl overflow-hidden">
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer text-white font-medium text-base list-none select-none hover:bg-[#fd6e4e]/10 transition-colors duration-200">
Can I use this with React or Next.js?
<span class="text-[#fd6e4e] transition-transform duration-300 group-open:rotate-180">
โผ
</span>
</summary>
<div>
<p class="px-5 py-4 text-gray-400 text-sm leading-relaxed">
Absolutely. You can convert this into a React component and manage state with `useState`. It works seamlessly with Next.js too โ just drop it into your component folder and you're good.
</p>
</div>
</details>
<!-- Accordion Item -->
<details class="group bg-[#1a1a1a] border border-[#fd6e4e]/30 rounded-xl overflow-hidden">
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer text-white font-medium text-base list-none select-none hover:bg-[#fd6e4e]/10 transition-colors duration-200">
Is this accessible for screen readers?
<span class="text-[#fd6e4e] transition-transform duration-300 group-open:rotate-180">
โผ
</span>
</summary>
<div>
<p class="px-5 py-4 text-gray-400 text-sm leading-relaxed">
Yes! Using `<details>` and `<summary>` gives you native browser accessibility support. Screen readers understand these elements out of the box, no extra ARIA hacks needed.
</p>
</div>
</details>
</div>
</body>
</html>
๐ก Pro Tip: The
groupclass in Tailwind lets you style child elements based on parent state.group-open:rotate-180rotates the arrow when<details>is open. Clean and zero-JS.
Option 2: React / Next.js Component (The Reusable Way)
This is how you'd build it in a real project โ a self-contained, reusable Accordion component.
// components/Accordion.jsx (or .tsx for TypeScript)
"use client"; // Required for Next.js App Router
import { useState, useRef } from "react";
const AccordionItem = ({ title, content }) => {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef(null);
return (
<div className="bg-[#1a1a1a] border border-[#fd6e4e]/30 rounded-xl overflow-hidden mb-3">
{/* Header / Trigger */}
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-5 py-4 text-white font-medium text-base cursor-pointer hover:bg-[#fd6e4e]/10 transition-colors duration-200 text-left"
aria-expanded={isOpen}
>
{title}
<span
className={`text-[#fd6e4e] transition-transform duration-300 ${
isOpen ? "rotate-180" : "rotate-0"
}`}
>
โผ
</span>
</button>
{/* Animated Body */}
<div
ref={contentRef}
style={{
maxHeight: isOpen ? `${contentRef.current?.scrollHeight}px` : "0px",
opacity: isOpen ? 1 : 0,
overflow: "hidden",
transition: "max-height 0.4s ease, opacity 0.35s ease",
}}
>
<p className="px-5 py-4 text-gray-400 text-sm leading-relaxed">
{content}
</p>
</div>
</div>
);
};
// Main Accordion Wrapper
const Accordion = ({ items }) => {
return (
<div className="w-full max-w-2xl mx-auto">
{items.map((item, index) => (
<AccordionItem key={index} title={item.title} content={item.content} />
))}
</div>
);
};
export default Accordion;
Usage:
// app/page.jsx or any page component
import Accordion from "@/components/Accordion";
const faqItems = [
{
title: "What is this accordion component?",
content: "It's a reusable, animated accordion built with React and Tailwind CSS. Drop it into any project and pass your data as props.",
},
{
title: "Does it work with Next.js App Router?",
content: "Yes โ just add 'use client' at the top of the component file since it uses React state and refs.",
},
{
title: "Can I customize the colors?",
content: "Absolutely. Just swap out the Tailwind color classes like bg-[#fd6e4e] with whatever fits your brand.",
},
];
export default function Home() {
return (
<main className="bg-[#0a0a0a] min-h-screen flex items-center justify-center p-6">
<Accordion items={faqItems} />
</main>
);
}
Why scrollHeight for the Animation?
Here's the insider detail most tutorials skip: you can't animate height: auto with CSS transitions. The browser literally doesn't know where auto ends, so it skips the animation.
The fix? Use max-height with a known value โ and if you want it to be dynamic (because your content length varies), you grab scrollHeight from the DOM ref. That gives you the exact pixel height of the content, so the transition is smooth every time, no matter how much text is inside.
This is the industry-standard approach. No hacks. No magic numbers. Just reliable, smooth animation.
Pros vs Cons: <details> vs React State
| Feature |
<details> + CSS |
React useState |
|---|---|---|
| JavaScript needed | โ None | โ Yes |
| Animation control | โ ๏ธ Limited (CSS only) | โ Full control |
| Accessibility | โ Native | โ With ARIA |
| Customizability | โ ๏ธ Some limits | โ Total freedom |
| React/Next.js projects | โ ๏ธ Works but less idiomatic | โ Perfect fit |
| Quick prototyping | โ Fastest | โ ๏ธ More setup |
| Browser support | โ All modern browsers | โ All modern browsers |
Rule of thumb: Use <details> for landing pages, static HTML, or when you want zero-JS simplicity. Use the React component when you're in a Next.js project and need full control over the animation, state, and behavior.
Best Tips & Do's / Don'ts
โ Do's
-
Use
scrollHeightfor dynamic content โ Never hardcodemax-heightvalues if your content length varies -
Add
overflow: hiddenon the wrapper โ Without this, content bleeds out before the animation finishes -
Use
aria-expandedon buttons โ Accessibility is not optional, it's professional - Test on mobile โ Accordion is heavily used on mobile; make sure the tap target (height of the header) is at least 44px
- Keep accordion headers short โ Users scan headings; if it wraps to 3 lines, rethink the copy
-
Use
transitionon bothmax-heightandopacityโ Fading the content slightly while it slides in looks way more polished
โ Don'ts
-
Don't use
display: none/display: blockfor animation โ You can't transition between these values - Don't nest accordions three levels deep โ One level of nesting is okay, two is pushing it, three is UX hell
- Don't auto-open more than one item by default โ It defeats the purpose of collapsing content
-
Don't forget the
list-noneclass on<summary>โ Browsers add a default disclosure triangle marker, which clashes with your custom arrow -
Don't use
heightfor transition โ Usemax-height. Period.
Common Mistakes Developers Make
1. Animating height: auto
This is the classic trap. You set transition: height 0.3s ease and nothing animates. The fix is max-height with a known value and overflow: hidden.
2. Forgetting overflow: hidden on the wrapper
The content slides in beautifully... but you can still see it before the animation starts. Always pair your animated wrapper with overflow: hidden.
3. Not using proper HTML semantics
Wrapping everything in <div>s and making them clickable with onClick is valid in React, but if you're writing plain HTML, use <details> and <summary>. It's semantic, accessible, and you get keyboard support for free.
4. Hardcoding max-height values
max-height: 200px works fine until someone adds more content and the text gets clipped. Use scrollHeight dynamically or set a generous fallback like max-height: 1000px (the transition will still work, it just won't be perfectly timed).
5. Making the close animation feel janky
The close animation often feels faster or different than open. This happens because max-height collapses instantly when the value snaps back. Adding a transition-timing-function: ease-in-out and matching durations on both states fixes this.
6. Not considering keyboard navigation
If you're using a <div> as a trigger instead of a <button>, keyboard users can't tab to it or press Enter to open it. Always use <button> for interactive elements โ it's not just best practice, it's correct.
Where Else Can You Use This? ๐
The accordion pattern is incredibly versatile. Here's where developers actually use it in production:
- FAQ Sections โ The most obvious use case. Stack 8โ10 questions, users expand what they need
- Filter Panels โ E-commerce sites use accordions for category filters on mobile
- Navigation Menus โ Mobile nav with expandable sub-menu items
- Settings Pages โ Group settings into collapsible categories (Profile, Security, Notifications)
- Documentation Sites โ Collapsible sections for long API docs or guides
- Product Specification Tables โ Show core specs upfront, hide the detailed breakdown in an accordion
- Onboarding Flows โ Step-by-step instructions where each step expands when active
- Invoice / Order Summaries โ Show totals, hide line item details until needed
Once you have a solid accordion component, you'll find yourself dropping it into projects constantly. It's one of those components that earns its place in every UI kit.
Conclusion: Stop Letting Your UI Snap Around ๐ฏ
You've got three ways to build this now โ semantic HTML with <details>, Tailwind + vanilla JS, and a full React/Next.js component. Each one is production-ready, accessible, and animated the right way.
The key takeaways:
- Use
max-height+overflow: hiddenfor animation โ notheight: auto - Use
scrollHeightin React for truly dynamic, reliable transitions - Use
<button>elements for triggers, not random<div>s - The
<details>+<summary>combo is massively underrated for static HTML projects
Building components like this โ the right way, with proper semantics and smooth transitions โ is what separates good frontend work from great frontend work.
Want more practical, copy-paste-ready code like this? I publish frontend tutorials, developer tips, and real-world UI builds regularly at hamidrazadev.com. No fluff, no filler โ just stuff that actually works.
If this helped you, share it with a developer friend who's still dealing with janky toggles. And if you've got a use case or a question, drop it in the comments or reach out on LinkedIn. Let's keep building better UIs together. ๐ช
Top comments (0)