9 CSS Accordion Animations — No Library, Pure CSS
A css accordion animation expands a header's panel to full height when it opens and collapses it to zero when it closes — nine versions here, plain CSS
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 height:auto via interpolate-size
- 02 Exclusive open via name attribute
- 03 Chevron marker rotate
- 04 Content fade + slide
- 05 Open item border highlight
- 06 Nested accordion
- 07 calc-size() fallback pattern
- 08 Elastic bounce open
- 09 Reused as sidebar nav
The order below follows how much control each panel hands to CSS alone. 01 through 03 are the core mechanics — how the height itself expands, how one panel closes its neighbor, how the arrow reports state. 04 through 06 dress that motion up with a content transition, a highlight, and nesting. 07 through 09 are the idea-form entries: a fallback for older browsers, a bounce borrowed from spring easing, and the same structure reused as a sidebar. Watch the grid above loop through all nine on its own — each one below is the 480×300 iframe version, running a two-second open-close cycle automatically since there is nothing inside it to click.
01height:auto via interpolate-size
The panel grows to its natural content height in one step, using interpolate-size: allow-keywords together with height: calc-size(auto, size) — the shortest path to css accordion animation dynamic height without ever reading scrollHeight in JavaScript. It's the base pattern the rest of this set builds on, useful for FAQ lists, terms-of-service details, and form help text. Support is still thin: MDN documents interpolate-size and calc-size() as shipping in Chrome 129 and missing from Safari and Firefox as of September 2026, so every demo below carries a plain height: auto fallback.
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height $duration $easing;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel { height: auto; }
}
02Exclusive open via name attribute
Closing whichever panel was open the moment another opens is, natively, one HTML attribute: grouping <details> elements under a shared name turns them into a radio-style set with no script at all, though browser support only reached Chrome 120, Safari 17.2, and Firefox 130. This demo reproduces the same closing behavior with plain buttons and two panels animated on opposite phases, which is the version to reach for on projects that still support older browsers. Settings menus and single-answer FAQs are the classic use.
.accordion__panel.is-demo-a { animation: accordion-loop-a $dur-loop $easing infinite; }
.accordion__panel.is-demo-b { animation: accordion-loop-b $dur-loop $easing infinite; }
@keyframes accordion-loop-a {
0%, 4% { height: 0; }
20%, 46% { height: calc-size(auto, size); }
62%, 100% { height: 0; }
}
@keyframes accordion-loop-b {
0%, 54% { height: 0; }
70%, 96% { height: calc-size(auto, size); }
100% { height: 0; }
}
03Chevron marker rotate
The header's chevron rotates 180 degrees exactly when the panel does, driven by transform: rotate() on the same class toggle instead of swapping between two separate icons. It's a small signal, but a header without it reads as unresponsive for the half-second before the panel visibly moves. Reach for it on accordion headers and dropdown menus, anywhere the trigger itself needs to show which way it's facing.
.accordion__chevron.is-demo {
animation: chevron-loop $dur-loop $easing infinite;
}
@keyframes chevron-loop {
0%, 8% { transform: rotate(45deg); }
42%, 58% { transform: rotate(-135deg); }
92%, 100% { transform: rotate(45deg); }
}
04Content fade + slide
The paragraph fades in while sliding down about 8 pixels from above, timed slightly behind the panel's own height change so the text never smears across a box that's still growing. It feels calmer than an instant paste-in, and it suits detail text and product option panels where the reveal should feel considered rather than mechanical.
.accordion__body.is-demo {
animation: content-loop $dur-loop $easing infinite;
}
@keyframes content-loop {
0%, 8% { opacity: 0; transform: translateY(-8px); }
30%, 70% { opacity: 1; transform: translateY(0); }
92%, 100% { opacity: 0; transform: translateY(-8px); }
}
05Open item border highlight
A 3px left border and a background shift call out whichever panel is currently open, running on the same rhythm as the height change so the highlight and the panel land together. Settings lists and any UI that needs to mark "you are here" among several sections benefit from a cue that doesn't rely on height alone to be noticed.
.accordion.is-demo-border {
animation: border-loop $dur-loop $easing infinite;
}
@keyframes border-loop {
0%, 8% { border-left-color: transparent; background: $subject-cream; }
42%, 58% { border-left-color: $color; background: $subject-cream; }
92%, 100% { border-left-color: transparent; background: $subject-cream; }
}
06Nested accordion
A second, smaller accordion sits inside the outer panel and only starts revealing once the outer one has mostly finished opening, so the two never compete for the same visual space at once. Multi-level FAQs and category tree menus are the obvious fit — anywhere a single question hides a few sub-answers underneath it.
.accordion--inner {
margin: 4px clamp(14px, 4vw, 20px) 12px;
background: rgba(255, 255, 255, .08);
border-radius: $r-control;
}
.accordion__panel--inner {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
@keyframes accordion-loop-inner {
0%, 20% { height: 0; }
32%, 46% { height: calc-size(auto, size); }
58%, 100% { height: 0; }
}
07calc-size() fallback pattern
Every rule that reads height: calc-size(auto, size) sits behind an @supports not (height: calc-size(auto, size)) block that swaps in a fixed pixel height instead, so a browser that doesn't understand the function still gets a transition, just a less exact one. It's the pattern to reach for on any form that has to keep working outside Chrome, which — per the support numbers under item 01 — is still most of the field.
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height $duration $easing;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel { height: auto; }
}
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
08Elastic bounce open
The panel opens along its normal height transition, but the content inside overshoots to 106% scale before easing back to 100%, so the whole thing reads as a small spring rather than a straight-line stop. Use it sparingly — a promo accordion or an onboarding card that wants a beat of extra attention, not a settings list a visitor opens forty times a session.
.accordion__body.is-demo {
transform-origin: top center;
animation: bounce-loop $dur-loop $easing infinite;
}
@keyframes bounce-loop {
0%, 8% { transform: scale(1); opacity: 0; }
36% { transform: scale(1.06); opacity: 1; }
50%, 58% { transform: scale(1); opacity: 1; }
92%, 100% { transform: scale(1); opacity: 0; }
}
09Reused as sidebar nav
The same header-plus-panel markup collapses a group of sidebar links instead of a paragraph, with no chevron and a little extra left padding so it reads as a nested list rather than a question-and-answer pair. Dashboard sidebars and documentation tables of contents reuse it as-is — the height trick doesn't care what's sitting inside the panel.
.sidebar__list {
list-style: none;
margin: 0;
padding: 0 clamp(14px, 4vw, 18px) 10px clamp(26px, 8vw, 32px);
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.sidebar__list.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
Where it breaks — the trap
The one to know before copying any of this: 06's inner accordion only opens smoothly if both panels — the inner one and the outer one wrapped around it — carry their own overflow: hidden and their own interpolate-size: allow-keywords. Drop overflow: hidden from either level and the nested panel's calc-size(auto, size) has nothing to clip against, so instead of growing on a curve it snaps straight to full height the instant the outer panel starts moving. That single missing line is the entire difference between item 06 reading as one motion and reading as two unrelated jumps. Grab the whole nested pattern from the zip instead of retyping two levels of panel by hand — the archive opens with 34fn5mpm, typed in exactly as it's written here.
Accessibility
Every one of the nine respects prefers-reduced-motion, but what gets left behind differs by item. 01, 02, 04 through 07, and 09 skip straight to the open or closed end state — no height transition, no fade, no border sweep — since a panel that still opens and closes on click loses nothing by doing it instantly. 08's bounce loses the most: the scale overshoot exists purely for delight, so reduced motion drops it to a flat opacity change with no scale at all. 03's chevron still flips instantly along with the panel, because the icon's job is to report state, not to entertain, and losing that would leave a reader guessing which way it points. MDN's guide to prefers-reduced-motion covers what triggers the query on each platform.
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
}
Every other CSS pattern on the site is filed in the CSS category; what GODRICH actually is — and isn't — sits on the about page.
FAQ
Do I need JavaScript to run any of these?
Only enough to toggle a class. Every open-close panel here is one click handler adding or removing is-open, and the height change, the chevron rotation, and the border highlight are all CSS transitions or animations underneath it. Item 02's exclusive-open behavior is the one exception worth naming: the script in that demo mimics what the native <details name="group"> attribute already does with no JavaScript at all, once your target browsers support it.
What's the real difference between this and the old max-height hack?
The old trick set max-height to something absurdly large, like 999px, and transitioned that instead of the panel's real height, so every accordion animated at a speed based on the guess rather than the content, and short panels felt sluggish next to tall ones. calc-size(auto, size) and interpolate-size read the panel's actual computed height and transition to that number directly, so a two-line answer and a two-hundred-line one both finish close to the same duration. That's the difference between a fixed-guess transition and a real css accordion animation dynamic height solution.
Can I drop these into a Tailwind or React project?
Yes, nothing here depends on a specific build tool. Tailwind's utility classes sit fine alongside this SCSS since the motion lives entirely in interpolate-size, calc-size(), and the keyframes rather than in any class Tailwind would also want to own, and the zip's react/ folder wraps each pattern in one component per effect. To test a single pattern before wiring it into a project, paste the vanilla SCSS into any accordion animation css codepen and toggle the class by hand.