Smooth Accordion
Panels open to their natural height and close again with no fixed max-height, while the chevron turns to match.
No. Most of these motions are plain CSS; GSAP only helps with timelines.
Yes. Every demo turns off or softens its movement under that setting.
Yes. Paste it into any AI coding tool to rebuild the effect.
Create an FAQ accordion in React and CSS that animates to the natural height of its content. Each item has a heading button with aria-expanded and aria-controls, and a panel region. Make the panel a grid with grid-template-rows: 0fr and an inner div with overflow: hidden and min-height: 0; when open, switch to grid-template-rows: 1fr. Transition grid-template-rows over 320ms with cubic-bezier(0.16, 1, 0.3, 1), and fade the inner content in. Rotate a chevron 180deg on open with the same timing. Allow only one item open at a time. Under prefers-reduced-motion, open and close instantly. The questions read "Do I need GSAP?", "Will it respect reduced motion?", and "Can I copy the prompt?".
Source
'use client'
import { useId, useState } from 'react'
import s from './demo.module.css'
export default function Accordion({ labels }: { labels: Record<string, string> }) {
const [open, setOpen] = useState(0)
const id = useId()
const items = [1, 2, 3].map((n) => ({ q: labels[`q${n}`], a: labels[`a${n}`] }))
return (
<div className={s.list}>
{items.map(({ q, a }, i) => (
<div key={q} className={s.item} data-open={open === i || undefined}>
<h3 className={s.heading}>
<button
type="button"
className={s.trigger}
aria-expanded={open === i}
aria-controls={`${id}-${i}`}
onClick={() => setOpen(open === i ? -1 : i)}
>
{q}
<svg className={s.chevron} viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9l6 6 6-6" />
</svg>
</button>
</h3>
<div id={`${id}-${i}`} role="region" className={s.panel}>
<div className={s.inner}>
<p className={s.answer}>{a}</p>
</div>
</div>
</div>
))}
</div>
)
}
.list {
width: min(320px, 100%);
border-top: 1px solid var(--stage-rule);
}
.item { border-bottom: 1px solid var(--stage-rule); }
.heading { margin: 0; }
.trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 11px 2px;
border: 0;
background: transparent;
color: var(--stage-ink);
font: 500 var(--text-sm) / 1.3 var(--font-body);
text-align: left;
cursor: pointer;
}
.trigger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: 4px; }
.chevron {
flex: none;
width: 16px;
height: 16px;
fill: none;
stroke: var(--stage-muted);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 320ms var(--ease-out);
}
.item[data-open] .chevron { transform: rotate(180deg); stroke: var(--stage-accent); }
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 320ms var(--ease-out);
}
.item[data-open] .panel { grid-template-rows: 1fr; }
.inner {
min-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 240ms var(--ease-out);
}
.item[data-open] .inner { opacity: 1; transition-delay: 80ms; }
.answer {
margin: 0;
padding: 0 2px 12px;
font: 400 var(--text-sm) / 1.5 var(--font-body);
color: var(--stage-muted);
}
@media (prefers-reduced-motion: reduce) {
.panel, .inner, .chevron { transition: none; }
}