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.
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: 0→0px(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:
Transitions only work between computed numeric values
0px → 100px✅0px → auto❌
height: autois an intrinsic keyword meaning "fit the content."- The browser doesn't know how to calculate the intermediate frames (e.g., "50% of auto") because
autodepends on the layout calculation which happens after the CSS is applied. Transitioning requires numeric start and end points.
- The browser doesn't know how to calculate the intermediate frames (e.g., "50% of auto") because
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:


