DEV Community

Cover image for How to Build a Smooth Accordion with Animation Using Tailwind CSS (Works with React, Next.js & Pure HTML)
Muhammad Hamid Raza
Muhammad Hamid Raza

Posted on

How to Build a Smooth Accordion with Animation Using Tailwind CSS (Works with React, Next.js & Pure HTML)

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 `&lt;details&gt;` and `&lt;summary&gt;` 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>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Pro Tip: The group class in Tailwind lets you style child elements based on parent state. group-open:rotate-180 rotates 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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 scrollHeight for dynamic content โ€” Never hardcode max-height values if your content length varies
  • Add overflow: hidden on the wrapper โ€” Without this, content bleeds out before the animation finishes
  • Use aria-expanded on 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 transition on both max-height and opacity โ€” Fading the content slightly while it slides in looks way more polished

โŒ Don'ts

  • Don't use display: none / display: block for 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-none class on <summary> โ€” Browsers add a default disclosure triangle marker, which clashes with your custom arrow
  • Don't use height for transition โ€” Use max-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: hidden for animation โ€” not height: auto
  • Use scrollHeight in 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)