Skip to main content

Command Palette

Search for a command to run...

Native HTML Accordions with Zero JavaScript: The Modern Way

Published
5 min read
Native HTML Accordions with Zero JavaScript: The Modern Way

We often reach for JavaScript libraries or heavy CSS hacks to build a simple accordion component. But with the latest browser capabilities, we can now achieve a fully functional, animated accordion using standard HTML and CSS alone.

Today, let's explore how to build this using the <details> element and modern CSS features like calc-size() and allow-discrete.

💡
At the time of writing(2025 December), calc-size function is only supported in Chromium-based browsers such as Chrome and Edge. Other browsers will require a small JavaScript fallback to achieve the same smooth height animations.

1. The Semantic Markup

<details> and <summary> are built-in HTML elements designed specifically for disclosure widgets—content that can be expanded and collapsed by the user.

💡 [MDN Reference] <details>: The Details disclosure element

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>HTML + CSS</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
     <section class="accordion">
      <details name="faq">
        <summary>Title 1</summary>
        <div class="panel">
          <div class="panel-content">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Doloremque
            sed tenetur explicabo minima reprehenderit, voluptatem cupiditate
            quibusdam qui perspiciatis, in accusamus. Reiciendis vero ab
            deserunt debitis, facere corporis dicta libero.
          </div>
        </div>
      </details>
      <details name="faq">
        <summary>Title 2</summary>
        <div class="panel">
          <div class="panel-content">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Doloremque
            sed tenetur explicabo minima reprehenderit, voluptatem cupiditate
            quibusdam qui perspiciatis, in accusamus. Reiciendis vero ab
            deserunt debitis, facere corporis dicta libero.
          </div>
        </div>
      </details>
      <details name="faq">
        <summary>Title 3</summary>
        <div class="panel">
          <div class="panel-content">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Doloremque
            sed tenetur explicabo minima reprehenderit, voluptatem cupiditate
            quibusdam qui perspiciatis, in accusamus. Reiciendis vero ab
            deserunt debitis, facere corporis dicta libero.
          </div>
        </div>
      </details>
    </section>
  </body>
</html>

At this point, the accordion already works—clicking a summary toggles its content.
But it’s visually unstyled and has no animation.

2. Basic Styling and Toggle Indicator

Let’s make it look presentable and add a visual toggle indicator.

We’ll style the summary::after pseudo-element to show a + when closed and a when open.

.accordion {
  width: 500px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
}

.accordion details {
  border-bottom: 1px solid #ddd;
}

.accordion details:last-child {
  border-bottom: none;
}

summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem;
  font-weight: 600;
  user-select: none;
}

/* Remove default styling */
summary::-webkit-details-marker {
  display: none;
}

/* (+/-) Button */
summary::after {
  content: "+";
  display: inline-flex;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  background: #f0f0f0;
  transition: transform 0.3s ease;
}

details[open] > summary::after {
  content: "-";
  transform: rotate(180deg);
}

.panel-content {
  padding: 1rem;
  color: #666;
  line-height: 1.6;
}

3. The Animation Problem: Why height: auto Doesn’t Animate

Naturally, we want the accordion to open and close smoothly.

So we try adding a transition:

details::details-content {
  height: 0;
  overflow: hidden;
  transition: height 0.3s;
}

But… nothing happens.
The accordion still snaps open and closed.

Why? 🤔

It’s because CSS transitions can only interpolate numeric values.

  • height: 00px (a concrete number)

  • height: auto → “as tall as the content” (an abstract concept)

The browser has no idea how to animate from 0 px to ??? px

There are two deeper reasons for this:

  1. Transitions only work between computed numeric values

    • 0px → 100px

    • 0px → auto

  2. height: auto is an intrinsic keyword meaning "fit the content."

    • The browser doesn't know how to calculate the intermediate frames (e.g., "50% of auto") because auto depends on the layout calculation which happens after the CSS is applied. Transitioning requires numeric start and end points.

4. The Solution: calc-size()

Enter calc-size(). This is a cutting-edge CSS function (introduced around late 2024) that allows us to perform calculations on intrinsic sizing keywords like auto, min-content, or max-content.

By using calc-size(auto, size), we effectively tell the browser: "Calculate what 'auto' creates in pixels, and use that numeric value for the animation."

details::details-content {
  height: 0;
  overflow: hidden;
}

details[open]::details-content {
  height: calc-size(auto, size);
}

Now the browser has a concrete, animatable target size, even though it’s derived from content. This spec is relatively new, currently supported in modern Chromium-based browsers.

💡 [MDN Reference] calc-size function

With this alone, the accordion opens smoothly.

5. Handling the Closing Animation (allow-discrete)

You might notice one last issue. The opening animation works beautifully, but when you close it, it snaps shut instantly.

The reason lies in how browsers treat the <details> element.

<details> is a user-agent–controlled element, meaning the browser is allowed to apply aggressive internal optimizations to its contents.

When a <details> element loses its open attribute, the browser internally determines that:

“This content cannot be shown to the user anymore.”

As a result, the browser removes the content from the render tree immediately. And once an element leaves the render tree, any running animations are effectively terminated.

To fix this, we need a way to delay that internal render-tree removal until the height animation finishes.

That’s exactly what transition-behavior: allow-discrete is for.

details::details-content {
  height: 0;
  overflow: hidden;

  transition:
    height 0.3s,
    content-visibility 0.3s;

  transition-behavior: allow-discrete;
}

details[open]::details-content {
  height: calc-size(auto, size);
}

This declaration tells the browser:

“If a discrete state change occurs here (such as rendering being turned on or off),
align that change with the transition timeline instead of applying it immediately.”

In practical terms, this means:

  • Don’t remove the content from the render tree the moment <details> closes.

  • Keep it rendered until the height transition completes.

  • Only then perform the internal cleanup.

With this in place, the accordion closes just as smoothly as it opens.


6. Browser Support and a Practical Fallback

There is one important caveat.

calc-size() is not yet supported across all browsers.
If smooth height animation is a hard requirement everywhere, a small JavaScript fallback is still necessary.

For readers, I have attached JavaScript code below.

Final Takeaway

This accordion works not because of clever hacks, but because it respects how browsers actually think about layout, rendering, and state.

calc-size() gives us animatable intrinsic sizes.
allow-discrete lets us align rendering lifecycles with transitions.
And <details> provides semantics and accessibility.

Check out the full demo below:

[Code]