Accordion with Smooth Height

Five hairline-ruled FAQ items open with a true height-from-zero-to-auto tween; the plus rotates into a cross, the copy slides up a beat later, the index turns lime and the previously open item closes at the same time.

Loading

AI prompt

Recreate this effect with any AI tool

Paste into Claude, Cursor, v0, Lovable or Bolt. 1287 words of exact values — rewritten live as you tune the Playground.

prompt.md · Accordion with Smooth Height
### EFFECT: Accordion with Smooth Height
**What it does (visual):** A two-column section: on the left a sticky intro (mono eyebrow, a large light serif heading "Questions, answered", a short muted lead); on the right a list of five FAQ rows separated by 1px hairlines. Each row is a full-width button: a small mono index ("01"…"05"), the question in a light serif, and a 2rem circular outline containing a thin "+" on the right. The first item is open on load: its index glows in the accent colour (#d4ff3a), the icon shows an "×", and its answer sits below the question in muted copy. Clicking a closed row makes its panel grow from 0 to its natural height over 0.6s with a smooth `power3.inOut` ease — no jump at the end — while the "+" rotates 45° into an "×" and, 0.15s in, the answer slides up 10px and fades in. In the same instant the previously open row collapses (its copy fades out in 0.25s, its height shrinks to 0 in 0.6s, its icon rotates back) so the list never has two panels open (unless multiple-open is enabled: false) and the total height changes smoothly. Clicking the open row closes it, leaving all five folded. Hovering a question tints it #d4ff3a and brightens the icon ring.

**Tech stack:**
- GSAP 3.13+ core only (no plugins).
- CDN: `<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>`
- npm: `npm i gsap` → `import gsap from "gsap";`

**HTML structure:**
```html
<section class="acc">
  <div class="acc__intro">
    <p class="acc__eyebrow">Layout · height: auto</p>
    <h1 class="acc__title">Questions, <em>answered</em></h1>
    <p class="acc__lead">Everything we are asked before a studio engagement begins.</p>
  </div>
  <div class="acc__list">
    <div class="acc__item is-open">
      <h2 class="acc__h">
        <button class="acc__trigger" type="button" id="acc-t1" aria-expanded="true" aria-controls="acc-p1">
          <span class="acc__index">01</span>
          <span class="acc__q">How does a project usually start?</span>
          <span class="acc__icon" aria-hidden="true"><i></i><i></i></span>
        </button>
      </h2>
      <div class="acc__panel" id="acc-p1" role="region" aria-labelledby="acc-t1">
        <div class="acc__body"><p>With a two-week discovery sprint…</p></div>
      </div>
    </div>
    <!-- 4 more .acc__item (ids acc-t2/acc-p2 … acc-t5/acc-p5, aria-expanded="false") -->
  </div>
</section>
```
The padding lives on `.acc__body`, never on `.acc__panel`, so the panel can genuinely be 0px tall when closed.

**CSS requirements:**
- `.acc`: `min-height: 100vh; max-width: 1400px; margin-inline: auto; padding: clamp(2.5rem, 7vw, 6rem) clamp(1rem, 5vw, 4rem); display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(2rem, 6vw, 6rem); align-items: start;`
- `.acc__intro`: `position: sticky; top: clamp(2rem, 7vw, 6rem);`
- `.acc__title`: display serif, `font-weight: 300; font-size: clamp(2.5rem, 5.5vw, 5rem); letter-spacing: -0.04em; line-height: .95;` `<em>` italic in `var(--muted)`. `.acc__lead`: `max-width: 36ch; color: var(--muted); line-height: 1.55`.
- `.acc__list { border-top: 1px solid var(--line) }`, `.acc__item { border-bottom: 1px solid var(--line) }`.
- `.acc__trigger`: `display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) 2rem; align-items: center; gap: clamp(.75rem, 2vw, 1.5rem); width: 100%; padding: clamp(1.1rem, 2vw, 1.6rem) 0; text-align: left; color: var(--text); background: none; border: 0;`
- `.acc__index`: mono 11px, `letter-spacing: .12em; color: var(--muted); transition: color .4s;` — `.acc__item.is-open .acc__index { color: var(--accent) }`.
- `.acc__q`: display serif 300, `font-size: clamp(1.25rem, 2.4vw, 2rem); letter-spacing: -0.03em; line-height: 1.1; transition: color .3s;` hover → `var(--accent)`.
- `.acc__icon`: `position: relative; width: 2rem; height: 2rem; border: 1px solid var(--line); border-radius: 50%;` each `<i>`: `position: absolute; left: 50%; top: 50%; width: 11px; height: 1px; background: var(--text); transform: translate(-50%, -50%);` second `<i>` additionally `rotate(90deg)`. Hover → `border-color: var(--text)`.
- `.acc__panel { height: 0; overflow: hidden; }` and `.acc__item.is-open .acc__panel { height: auto; }` — the class owns the resting state, JS only owns the transition.
- `.acc__body { padding: 0 2rem clamp(1.25rem, 2.5vw, 2rem) 3.5rem; }` `p { max-width: 58ch; color: var(--muted); line-height: 1.6; }`
- Focus: `.acc__trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 4px; }`
- Accent colour (open index, hover tint, focus ring): `--accent: #d4ff3a`.

**Animation spec (exact values):**
1. Initial state (`gsap.set`, per item): open item → `panel { height: 'auto' }`, `body { y: 0, opacity: 1 }`, `icon { rotate: 45 }`; closed items → `panel { height: 0 }`, `body { y: 10, opacity: 0 }`, `icon { rotate: 0 }`.
2. Intro (not under reduced motion): `gsap.from('.acc__intro > *', { y: 24, opacity: 0, duration: 0.9, ease: 'power3.out', stagger: 0.08, delay: 0.05 })` and `gsap.from(items, { y: 16, opacity: 0, duration: 0.8, ease: 'power3.out', stagger: 0.06, delay: 0.2, clearProps: 'transform' })`.
3. `open(item)` — add `.is-open`, set `aria-expanded="true"`, then a timeline (`defaults: { overwrite: 'auto' }`):
   - `tl.to(panel, { height: 'auto', duration: 0.6, ease: 'power3.inOut', clearProps: 'height' }, 0)` — target the panel, `height 0 → auto`, inline height cleared on complete so the open panel reflows on resize.
   - `tl.to(icon, { rotate: 45, duration: 0.5, ease: 'power3.inOut' }, 0)` — plus → cross.
   - `tl.fromTo(body, { y: 10, opacity: 0 }, { y: 0, opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.15)` — copy slides up, starting 0.15s in.
4. `close(item)` — first `gsap.set(panel, { height: panel.offsetHeight })` (pin the current px height so removing the class does not snap it to 0), then remove `.is-open`, set `aria-expanded="false"`, then:
   - `tl.to(body, { opacity: 0, y: 10, duration: 0.25, ease: 'power2.in' }, 0)`
   - `tl.to(icon, { rotate: 0, duration: 0.5, ease: 'power3.inOut' }, 0)`
   - `tl.to(panel, { height: 0, duration: 0.6, ease: 'power3.inOut' }, 0)`
5. `toggle(item)`: if it is already open → `close(item)`; else — unless multiple panels may stay open (`MULTI_OPEN = false`) — `close()` every currently open item and `open(item)` in the same call so both tweens run simultaneously.
6. The index colour change is a CSS transition (`color .4s`) driven by the `.is-open` class.

**Interaction:** Click toggles; one item open at a time (or several when `MULTI_OPEN` is false). Hover tints the question #d4ff3a and the icon ring to `var(--text)` over 0.3s. Keyboard: Enter/Space activate (native button), ArrowDown/ArrowUp move focus between triggers (wrapping), Home/End jump to first/last.

**Responsive behavior:** Under 768px the section becomes a single column (`grid-template-columns: 1fr; gap: 2.5rem`), the intro is no longer sticky, the trigger grid tightens to `2.25rem minmax(0,1fr) 1.75rem`, the icon shrinks to 1.75rem and `.acc__body` padding becomes `0 0 … 2.25rem`. All sizes are `clamp()`-based so the layout holds at 375px. No `gsap.matchMedia()` branch is required: the animation reads measured heights, so it is identical on touch.

**Accessibility:** The pattern is the WAI-ARIA disclosure: each trigger is a `<button>` inside an `<h2>`, with `aria-expanded` and `aria-controls` pointing at a panel that has `role="region"` and `aria-labelledby` back to the button. Under `prefers-reduced-motion: reduce` every open/close is an instant `gsap.set` (height `auto`/`0`, opacity `1`/`0`, rotate `45`/`0`) and the CSS transitions on index, question and icon are disabled. Focus ring is a 2px `--accent` outline.

**Performance rules:** `height` is the one non-transform property, animated only on the small panel element with `overflow: hidden` — the cost is a single layout per frame for 0.6s, acceptable for a disclosure. Everything else is `transform`/`opacity`. `overwrite: 'auto'` prevents stacked tweens on rapid clicks. All tweens are created inside `gsap.context()` and `ctx.revert()` is called on unmount.

**Customization knobs:**
- `OPEN_DURATION` (0.6) and `EASE` (`power3.inOut`) — the breathing speed; `expo.inOut` is snappier.
- Body delay (0.15) and travel (`y: 10`).
- Icon rotation (45°) — use 135° for a spin-and-cross.
- `MULTI_OPEN` (false) — when true, opening an item no longer closes the others.
- Index highlight colour (`--accent`: #d4ff3a).

**Acceptance criteria:**
- [ ] On load exactly one item is open with a #d4ff3a index and an "×" icon; the others are 0px tall.
- [ ] Opening tweens height from 0 to natural height in 0.6s with no end jump; the copy begins fading 0.15s after the panel starts.
- [ ] The previously open item closes at the same moment the new one opens (unless `MULTI_OPEN` is true).
- [ ] After opening, resizing the window reflows the open panel (no fixed inline height remains).
- [ ] `aria-expanded` always reflects state; arrow keys move focus between questions.
- [ ] Reduced motion: toggles are instant; no console errors.

Source

Full source code

<section class="acc">
  <div class="acc__intro">
    <p class="acc__eyebrow">Layout · height: auto</p>
    <h1 class="acc__title">Questions, <em>answered</em></h1>
    <p class="acc__lead">Everything we are asked before a studio engagement begins. One panel open at a time; the rest stay folded.</p>
  </div>

  <div class="acc__list">
    <div class="acc__item is-open">
      <h2 class="acc__h"><button class="acc__trigger" type="button" id="acc-t1" aria-expanded="true" aria-controls="acc-p1"><span class="acc__index">01</span><span class="acc__q">How does a project usually start?</span><span class="acc__icon" aria-hidden="true"><i></i><i></i></span></button></h2>
      <div class="acc__panel" id="acc-p1" role="region" aria-labelledby="acc-t1"><div class="acc__body"><p>With a two-week discovery sprint. We interview your team, audit what exists and return with a written point of view, a scope and a fixed price. If we are not the right studio, we say so then.</p></div></div>
    </div>
    <div class="acc__item">
      <h2 class="acc__h"><button class="acc__trigger" type="button" id="acc-t2" aria-expanded="false" aria-controls="acc-p2"><span class="acc__index">02</span><span class="acc__q">What does a typical engagement cost?</span><span class="acc__icon" aria-hidden="true"><i></i><i></i></span></button></h2>
      <div class="acc__panel" id="acc-p2" role="region" aria-labelledby="acc-t2"><div class="acc__body"><p>Identity programmes begin at a fixed fee, websites are quoted per sprint, and ongoing design partnership is a monthly retainer. Every proposal lists exactly what ships, so there are no surprises at the invoice stage.</p></div></div>
    </div>
    <div class="acc__item">
      <h2 class="acc__h"><button class="acc__trigger" type="button" id="acc-t3" aria-expanded="false" aria-controls="acc-p3"><span class="acc__index">03</span><span class="acc__q">Who will I actually work with?</span><span class="acc__icon" aria-hidden="true"><i></i><i></i></span></button></h2>
      <div class="acc__panel" id="acc-p3" role="region" aria-labelledby="acc-t3"><div class="acc__body"><p>The people in the pitch are the people on the project. A creative director, a designer and an engineer stay with you from kick-off to launch, and you have their direct lines.</p></div></div>
    </div>
    <div class="acc__item">
      <h2 class="acc__h"><button class="acc__trigger" type="button" id="acc-t4" aria-expanded="false" aria-controls="acc-p4"><span class="acc__index">04</span><span class="acc__q">Do you build what you design?</span><span class="acc__icon" aria-hidden="true"><i></i><i></i></span></button></h2>
      <div class="acc__panel" id="acc-p4" role="region" aria-labelledby="acc-t4"><div class="acc__body"><p>Yes. Motion, front-end and CMS integration are done in-house, which is why the launched site looks like the prototype. We hand over documented code, not a Figma file and good wishes.</p></div></div>
    </div>
    <div class="acc__item">
      <h2 class="acc__h"><button class="acc__trigger" type="button" id="acc-t5" aria-expanded="false" aria-controls="acc-p5"><span class="acc__index">05</span><span class="acc__q">How long until launch?</span><span class="acc__icon" aria-hidden="true"><i></i><i></i></span></button></h2>
      <div class="acc__panel" id="acc-p5" role="region" aria-labelledby="acc-t5"><div class="acc__body"><p>Most identity and website programmes run ten to fourteen weeks. Campaign work can move in days. We would rather tell you an honest date than a flattering one.</p></div></div>
    </div>
  </div>
</section>