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
01 accordion-height-auto-interpolate
02 accordion-exclusive-open
03 accordion-chevron-rotate
04 accordion-fade-slide-content
05 accordion-border-highlight-open
06 accordion-nested-accordion
07 accordion-calc-size-fallback
08 accordion-elastic-bounce-open
09 accordion-sidebar-nav-reuse
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. Once the expanding thing becomes navigation, as in 09, the icon has to answer too — that's the CSS hamburger menu animations — and when a list floats over the page instead of pushing it down, the CSS dropdown menu animations cover that shape.
01height:auto via interpolate-size
The motion
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Refund policy</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body">Refunds within 7 days of purchase.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 열리는 데 걸리는 시간
$easing: $ease-spring; // 전환 곡선
$color: $subject-blue; // 강조 색
@include stage($stage-ink);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-ink;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-ink;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #17141a;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #17141a;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
document.querySelectorAll('.accordion').forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionHeightAuto.module.scss";
export type AccordionHeightAutoProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionHeightAuto({
header = "Refund policy",
children = "Refunds within 7 days of purchase.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
defaultOpen = false,
}: AccordionHeightAutoProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}01 · height:auto via interpolate-size — Measured area 11.691% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where a css accordion effect fits
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
What it does
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.

<main class="stage">
<section class="accordion-group">
<div class="accordion accordion--a">
<div class="accordion__header" role="button" tabindex="0" aria-expanded="false">
<span class="accordion__title">How long does shipping take?</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</div>
<div class="accordion__panel is-demo-a">
<p class="accordion__body">Usually 2-3 days.</p>
</div>
</div>
<div class="accordion accordion--b">
<div class="accordion__header" role="button" tabindex="0" aria-expanded="false">
<span class="accordion__title">How do I request an exchange?</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</div>
<div class="accordion__panel is-demo-b">
<p class="accordion__body">Request it from My Page.</p>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-yellow);
.accordion-group {
width: clamp(220px, 78vw, 340px);
display: flex;
flex-direction: column;
gap: 10px;
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-ink;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-cream;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-cream;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.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; }
}
// 데모: name 속성처럼 하나가 열리는 동안 나머지는 닫혀 있다 (2초 주기, 위상만 반대)
.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; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop-a { 0%, 4% { height: 0; } 20%, 46% { height: 160px; } 62%, 100% { height: 0; } }
@keyframes accordion-loop-b { 0%, 54% { height: 0; } 70%, 96% { height: 160px; } 100% { height: 0; } }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion-group {
width: clamp(220px, 78vw, 340px);
display: flex;
flex-direction: column;
gap: 10px;
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #fff7e6;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #fff7e6;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 220ms ease-out;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo-a {
animation: accordion-loop-a 2s ease-out infinite;
}
.accordion__panel.is-demo-b {
animation: accordion-loop-b 2s ease-out 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;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop-a {
0%, 4% {
height: 0;
}
20%, 46% {
height: 160px;
}
62%, 100% {
height: 0;
}
}
@keyframes accordion-loop-b {
0%, 54% {
height: 0;
}
70%, 96% {
height: 160px;
}
100% {
height: 0;
}
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
var group = document.querySelector('.accordion-group');
var accs = Array.prototype.slice.call(group.querySelectorAll('.accordion'));
accs.forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo-a', 'is-demo-b');
var willOpen = !acc.classList.contains('is-open');
accs.forEach(function (other) {
other.classList.remove('is-open');
other.querySelector('.accordion__header').setAttribute('aria-expanded', 'false');
other.querySelector('.accordion__panel').classList.remove('is-demo-a', 'is-demo-b');
});
if (willOpen) {
acc.classList.add('is-open');
header.setAttribute('aria-expanded', 'true');
}
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionExclusiveOpen.module.scss";
export type AccordionExclusiveOpenProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionExclusiveOpen({
header = "How long does shipping take?",
children = "Usually 2-3 days.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
defaultOpen = false,
}: AccordionExclusiveOpenProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}02 · Exclusive open via name attribute — Measured area 32.087% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where it fits
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
What you see
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Payment methods</span>
<span class="accordion__chevron is-demo" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body">We accept cards and bank transfer.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-orange);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-ink;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-ink;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
// 화살표가 패널과 같은 리듬으로 180도 돈다
.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); }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #17141a;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #17141a;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
.accordion__chevron.is-demo {
animation: chevron-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes chevron-loop {
0%, 8% {
transform: rotate(45deg);
}
42%, 58% {
transform: rotate(-135deg);
}
92%, 100% {
transform: rotate(45deg);
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
var acc = document.querySelector('.accordion');
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
var chevron = acc.querySelector('.accordion__chevron');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
chevron.classList.remove('is-demo');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
chevron.style.transform = open ? 'rotate(-135deg)' : 'rotate(45deg)';
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionChevronRotate.module.scss";
export type AccordionChevronRotateProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionChevronRotate({
header = "Payment methods",
children = "We accept cards and bank transfer.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
defaultOpen = false,
}: AccordionChevronRotateProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}03 · Chevron marker rotate — Measured area 18.795% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where it fits
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 motion
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Product details</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body is-demo">Made of 100% cotton.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-ink;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-cream;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-cream;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
// 본문이 살짝 위에서 내려오며 페이드인 (패널이 열리는 구간과 같은 리듬)
.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); }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #fff7e6;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #fff7e6;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 600ms ease-out;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s ease-out infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
.accordion__body.is-demo {
animation: content-loop 2s ease-out 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);
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
document.querySelectorAll('.accordion').forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionFadeSlideContent.module.scss";
export type AccordionFadeSlideContentProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionFadeSlideContent({
header = "Product details",
children = "Made of 100% cotton.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
defaultOpen = false,
}: AccordionFadeSlideContentProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}04 · Content fade + slide — Measured area 18.717% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password
Good places for it
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
What it does
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.

<main class="stage">
<section class="accordion is-demo-border">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Membership tier perks</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body">Points rates vary by tier.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-cream;
border-radius: $r-card;
border-left: 3px solid transparent;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-ink;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none; width: 9px; height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-ink;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
// 열려 있는 동안만 왼쪽 테두리와 배경이 강조된다 (패널과 같은 리듬)
.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; }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #fff7e6;
border-radius: 16px;
border-left: 3px solid transparent;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #17141a;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #17141a;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
.accordion.is-demo-border {
animation: border-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes border-loop {
0%, 8% {
border-left-color: transparent;
background: #fff7e6;
}
42%, 58% {
border-left-color: #2f6df6;
background: #fff7e6;
}
92%, 100% {
border-left-color: transparent;
background: #fff7e6;
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
var acc = document.querySelector('.accordion');
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
acc.classList.remove('is-demo-border');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionBorderHighlight.module.scss";
export type AccordionBorderHighlightProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionBorderHighlight({
header = "Membership tier perks",
children = "Points rates vary by tier.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
defaultOpen = false,
}: AccordionBorderHighlightProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}05 · Open item border highlight — Measured area 12.473% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password
When to use it
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
What you see
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Frequently asked questions</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<div class="accordion accordion--inner">
<button class="accordion__header accordion__header--inner" type="button" aria-expanded="false">
<span class="accordion__title">About shipping</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel accordion__panel--inner is-demo-inner">
<p class="accordion__body">Jeju Island has an extra shipping fee.</p>
</div>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-yellow);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-ink;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-cream;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-cream;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
.accordion--inner {
margin: 4px clamp(14px, 4vw, 20px) 12px;
background: rgba(255, 255, 255, .08);
border-radius: $r-control;
}
.accordion__header--inner {
padding: 10px 12px;
font-weight: 600;
font-size: clamp(12px, 3vw, 14px);
line-height: 1.3;
font-family: inherit;
}
.accordion__panel--inner {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion__panel--inner .accordion__body {
padding: 0 12px 10px;
}
// 안쪽 아코디언은 바깥이 열려 있는 구간(8%~58%) 안에서 더 짧게 열린다
.accordion__panel--inner.is-demo-inner {
animation: accordion-loop-inner $dur-loop $easing infinite;
}
@keyframes accordion-loop-inner {
0%, 20% { height: 0; }
32%, 46% { height: calc-size(auto, size); }
58%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop-inner { 0%, 20% { height: 0; } 32%, 46% { height: 90px; } 58%, 100% { height: 0; } }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #fff7e6;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #fff7e6;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 220ms ease-out;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s ease-out infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
.accordion--inner {
margin: 4px clamp(14px, 4vw, 20px) 12px;
background: rgba(255, 255, 255, 0.08);
border-radius: 12px;
}
.accordion__header--inner {
padding: 10px 12px;
font-weight: 600;
font-size: clamp(12px, 3vw, 14px);
line-height: 1.3;
font-family: inherit;
}
.accordion__panel--inner {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion__panel--inner .accordion__body {
padding: 0 12px 10px;
}
.accordion__panel--inner.is-demo-inner {
animation: accordion-loop-inner 2s ease-out infinite;
}
@keyframes accordion-loop-inner {
0%, 20% {
height: 0;
}
32%, 46% {
height: calc-size(auto, size);
}
58%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop-inner {
0%, 20% {
height: 0;
}
32%, 46% {
height: 90px;
}
58%, 100% {
height: 0;
}
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
document.querySelectorAll('.accordion').forEach(function (acc) {
var header = acc.querySelector(':scope > .accordion__header');
var panel = acc.querySelector(':scope > .accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo', 'is-demo-inner');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionNestedAccordion.module.scss";
export type AccordionNestedAccordionProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionNestedAccordion({
header = "Frequently asked questions",
children = "About shipping: Jeju Island has an extra fee.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
defaultOpen = false,
}: AccordionNestedAccordionProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}06 · Nested accordion — Measured area 37.072% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where it fits
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
The motion
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Terms of service</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body">Terms may change without prior notice.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-orange);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-ink;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-ink;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
// calc-size() 를 모르면 이 규칙이 이긴다 (같은 클래스, 고정 높이로 대체)
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
// 실사용: 클릭하면 열림 상태(.is-open)가 붙고 목표 높이까지 전환된다
.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; }
}
// 데모: 2초 주기로 스스로 열고 닫힌다. WAAPI 시계로 재생 위치를 잡으려면
// transition 이 아니라 keyframes 여야 한다 (blog-demo 측정 규칙)
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #17141a;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #17141a;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
document.querySelectorAll('.accordion').forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionCalcSizeFallback.module.scss";
export type AccordionCalcSizeFallbackProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionCalcSizeFallback({
header = "Terms of service",
children = "Terms may change without prior notice.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
defaultOpen = false,
}: AccordionCalcSizeFallbackProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}07 · calc-size() fallback pattern — Measured area 18.438% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password
Good places for it
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
What it does
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.

<main class="stage">
<section class="accordion">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">New member perks</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<div class="accordion__panel is-demo">
<p class="accordion__body is-demo">Get a 10% coupon on your first purchase.</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-pop; // 튕기는 느낌의 오버슈트 곡선
$color: $subject-blue;
@include stage($stage-paper);
.accordion {
width: clamp(220px, 78vw, 340px);
background: $subject-ink;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: $subject-cream;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: $subject-cream;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.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; }
}
// 데모: 열릴 때 안쪽 카드가 살짝 커졌다가($ease-pop) 자리를 잡는다
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop { 0%, 8% { height: 0; } 42%, 58% { height: 220px; } 92%, 100% { height: 0; } }
}
.accordion__body.is-demo {
transform-origin: top center; // 규칙8: 헤더 아래 변 붙어 펼침 팝
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; }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion {
width: clamp(220px, 78vw, 340px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 18px) clamp(14px, 4vw, 20px);
cursor: pointer;
color: #fff7e6;
font-weight: 600;
font-size: clamp(14px, 3.6vw, 17px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__body {
margin: 0;
padding: 0 clamp(14px, 4vw, 20px) clamp(12px, 3vw, 18px);
color: #fff7e6;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14.5px);
line-height: 1.5;
font-family: inherit;
}
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 600ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
.accordion__panel.is-demo {
animation: accordion-loop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 220px;
}
92%, 100% {
height: 0;
}
}
}
.accordion__body.is-demo {
transform-origin: top center;
animation: bounce-loop 2s cubic-bezier(0.2, 1.4, 0.4, 1) 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;
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
document.querySelectorAll('.accordion').forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
var panel = acc.querySelector('.accordion__panel');
header.addEventListener('click', function () {
panel.classList.remove('is-demo');
var open = acc.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
});
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionElasticBounce.module.scss";
export type AccordionElasticBounceProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionElasticBounce({
header = "New member perks",
children = "Get a 10% coupon on your first purchase.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
defaultOpen = false,
}: AccordionElasticBounceProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}08 · Elastic bounce open — Measured area 19.022% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password
When to use it
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
What you see
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.

<main class="stage">
<nav class="accordion sidebar">
<button class="accordion__header" type="button" aria-expanded="false">
<span class="accordion__title">Settings</span>
<span class="accordion__chevron" aria-hidden="true"></span>
</button>
<ul class="sidebar__list is-demo">
<li>Account</li>
<li>Notifications</li>
<li>Payment method</li>
</ul>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-ink);
.accordion.sidebar {
width: clamp(200px, 70vw, 280px);
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 16px) clamp(14px, 4vw, 18px);
cursor: pointer;
color: $subject-ink;
font-weight: 600;
font-size: clamp(13px, 3.4vw, 15px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none; width: 8px; height: 8px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.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 li {
padding: 6px 0;
color: $subject-ink;
opacity: .86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.sidebar__list.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: calc-size(auto, size); }
92%, 100% { height: 0; }
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop { 0%, 8% { height: 0; } 42%, 58% { height: 130px; } 92%, 100% { height: 0; } }
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.accordion.sidebar {
width: clamp(200px, 70vw, 280px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
font-family: inherit;
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
background: none;
padding: clamp(12px, 3vw, 16px) clamp(14px, 4vw, 18px);
cursor: pointer;
color: #17141a;
font-weight: 600;
font-size: clamp(13px, 3.4vw, 15px);
line-height: 1.3;
font-family: inherit;
text-align: left;
}
.accordion__chevron {
flex: none;
width: 8px;
height: 8px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.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 li {
padding: 6px 0;
color: #17141a;
opacity: 0.86;
font-weight: 400;
font-size: clamp(12px, 3.2vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.sidebar__list.is-demo {
animation: accordion-loop 2s ease-out infinite;
}
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: calc-size(auto, size);
}
92%, 100% {
height: 0;
}
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% {
height: 0;
}
42%, 58% {
height: 130px;
}
92%, 100% {
height: 0;
}
}
}
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__panel.is-demo,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo {
height: auto;
}
}(function () {
var nav = document.querySelector('.sidebar');
var header = nav.querySelector('.accordion__header');
var list = nav.querySelector('.sidebar__list');
header.addEventListener('click', function () {
list.classList.remove('is-demo');
var open = nav.classList.toggle('is-open');
header.setAttribute('aria-expanded', String(open));
list.style.height = open ? 'calc-size(auto, size)' : '0';
});
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AccordionSidebarNav.module.scss";
export type AccordionSidebarNavProps = {
/** 헤더에 보이는 제목 */
header?: ReactNode;
/** 열렸을 때 보이는 본문 */
children?: ReactNode;
/** height:auto 전환 시간 (기본 300ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 강조 색 */
color?: string;
defaultOpen?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AccordionSidebarNav({
header = "Settings",
children = "Account, Notifications, Payment method.",
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
defaultOpen = false,
}: AccordionSidebarNavProps) {
const [open, setOpen] = useState(defaultOpen);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.accordion} style={vars}>
<button
type="button"
className={styles.header}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span>{header}</span>
<span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} aria-hidden="true" />
</button>
<div className={`${styles.panel} ${open ? styles.panelOpen : ""}`}>
<p className={styles.body}>{children}</p>
</div>
</section>
);
}09 · Reused as sidebar nav — Measured area 20.065% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where it fits
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
Nested panels need their own overflow
An accordion animation css codepen won't warn you about this one: 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.
A long FAQ folded into panels like these still makes for a tall page once a few are open, which is why the way back up matters — the scroll-driven version of that button is in the back to top button CSS set.
Accessibility
Reduced motion
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;
}
}
More css accordion examples and every other CSS pattern on the site are 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.
How does a css accordion animation dynamic height differ from 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. If 05's "this one is open" border is the part you want on a grid of cards rather than a stack of panels, the same signal handled on hover sits in the CSS card hover effects.









