Warm Editorial — disclosure
Each item is its own rounded card with a 12px gap, rather than rows sharing hairline dividers. Built on real buttons with aria-expanded and aria-controls, and animated with grid-template-rows so the panel height is never hard-coded.
Paste this into Claude, Cursor or Copilot — or point the agent at
https://ui-deploy.com/r/warm-editorial/accordion/prompt.md
# FAQ Accordion — Warm Editorial
Paste this whole file into Claude, Cursor or Copilot.
---
## What to build
A disclosure list where **each item is its own rounded card** with a 12px gap
between items — not a stack of rows sharing hairline dividers. 24px corners.
Each row: question on the left, a circled `+` on the right that rotates 45° into
an `×` when the item opens.
---
## Tokens
```css
:root{
--bg2:#F6F3ED; --bg3:#E4DED2; --border:#DAD3C6;
--text:#22201D; --text2:#5C574F;
--accent:#A38A6A; --accent-text:#7D6644;
--accent-s:rgba(163,138,106,.12); --accent-b:rgba(163,138,106,.30);
--radius-lg:24px;
--font-display:'Inter Tight',system-ui,sans-serif;
--font-body:'Inter',system-ui,sans-serif;
--ease:cubic-bezier(.22,.61,.36,1);
}
[data-theme="dark"]{
--bg2:#1E1A17; --bg3:#262119; --border:#2E2822;
--text:#EDE7DD; --text2:#A79E92;
--accent:#B39B7C; --accent-text:#C0A888;
--accent-s:rgba(179,155,124,.13); --accent-b:rgba(179,155,124,.28);
}
```
---
## Rules — these are not preferences
1. **Use a real `<button>` with `aria-expanded` and `aria-controls`.** A `<div>`
with a click handler is not a disclosure control. Wrap it in a heading
(`<h3>`) so the questions appear in the document outline.
2. **Reset the button's default padding.** A `<button>` spanning a card row keeps
its UA padding otherwise, and it shows up as an unexplained inset that is
maddening to debug.
3. **Animate with `grid-template-rows: 0fr → 1fr`**, with `overflow:hidden` on the
inner wrapper. Do not measure `scrollHeight` in JS and do not animate
`max-height` to a guessed value.
4. **Put `hidden` back on after the collapse finishes**, not at click time — remove
it before opening. Toggling it immediately makes the panel snap instead of
animating, and leaving it off keeps collapsed content in the tab order.
5. **The `+` rotates 45°.** No chevron, no caret, no icon swap.
6. **Cards separate, never dividers.** A shared-divider list is a different
language; this one breathes.
7. Icons are inline SVG, `stroke="currentColor"`, `fill="none"`. No emoji.
8. `:focus-visible` sets `outline` only — never `border-radius`.
---
## Variants
- **One open at a time** — before opening, close any sibling with
`aria-expanded="true"`. Multiple-open is the default here because FAQ answers
are usually compared, not read in sequence.
- **Answer with a link or list** — put it inside `.we-acc-inner`; the padding
already accounts for it.
---
## Reference implementation
See `code.html` — 16 lines of dependency-free JS.
Paste this whole file into Claude, Cursor or Copilot.
A disclosure list where each item is its own rounded card with a 12px gap between items — not a stack of rows sharing hairline dividers. 24px corners.
Each row: question on the left, a circled + on the right that rotates 45° into an × when the item opens.
:root{
--bg2:#F6F3ED; --bg3:#E4DED2; --border:#DAD3C6;
--text:#22201D; --text2:#5C574F;
--accent:#A38A6A; --accent-text:#7D6644;
--accent-s:rgba(163,138,106,.12); --accent-b:rgba(163,138,106,.30);
--radius-lg:24px;
--font-display:'Inter Tight',system-ui,sans-serif;
--font-body:'Inter',system-ui,sans-serif;
--ease:cubic-bezier(.22,.61,.36,1);
}
[data-theme="dark"]{
--bg2:#1E1A17; --bg3:#262119; --border:#2E2822;
--text:#EDE7DD; --text2:#A79E92;
--accent:#B39B7C; --accent-text:#C0A888;
--accent-s:rgba(179,155,124,.13); --accent-b:rgba(179,155,124,.28);
}
<button> with aria-expanded and aria-controls. A <div> with a click handler is not a disclosure control. Wrap it in a heading (<h3>) so the questions appear in the document outline.<button> spanning a card row keeps its UA padding otherwise, and it shows up as an unexplained inset that is maddening to debug.grid-template-rows: 0fr → 1fr, with overflow:hidden on the inner wrapper. Do not measure scrollHeight in JS and do not animate max-height to a guessed value.hidden back on after the collapse finishes, not at click time — remove it before opening. Toggling it immediately makes the panel snap instead of animating, and leaving it off keeps collapsed content in the tab order.+ rotates 45°. No chevron, no caret, no icon swap.stroke="currentColor", fill="none". No emoji.:focus-visible sets outline only — never border-radius. aria-expanded="true". Multiple-open is the default here because FAQ answers are usually compared, not read in sequence.
.we-acc-inner; the paddingalready accounts for it.
See code.html — 16 lines of dependency-free JS.
<!-- Warm Editorial — FAQ accordion
Requires tokens.css. Separate cards with a gap, not a divider stack. -->
<div class="we-acc">
<div class="we-acc-item">
<h3>
<button class="we-acc-trigger" type="button" aria-expanded="false" aria-controls="we-acc-p1">
<span>How long does a first session take?</span>
<span class="we-acc-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
</span>
</button>
</h3>
<div class="we-acc-panel" id="we-acc-p1" role="region" hidden>
<div class="we-acc-inner">
<p>Ninety minutes. The first thirty are assessment — we measure before we
prescribe anything, and you leave with the numbers written down.</p>
</div>
</div>
</div>
<div class="we-acc-item">
<h3>
<button class="we-acc-trigger" type="button" aria-expanded="false" aria-controls="we-acc-p2">
<span>Do I need to be an athlete?</span>
<span class="we-acc-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
</span>
</button>
</h3>
<div class="we-acc-panel" id="we-acc-p2" role="region" hidden>
<div class="we-acc-inner">
<p>No. Most people who come here sit at a desk. The protocols are built
around a nervous system under load, whatever put it there.</p>
</div>
</div>
</div>
<div class="we-acc-item">
<h3>
<button class="we-acc-trigger" type="button" aria-expanded="false" aria-controls="we-acc-p3">
<span>Can I book a single visit?</span>
<span class="we-acc-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
</span>
</button>
</h3>
<div class="we-acc-panel" id="we-acc-p3" role="region" hidden>
<div class="we-acc-inner">
<p>Yes, and it is the honest way to start. Packages exist because recovery
compounds, not because we want you locked in.</p>
</div>
</div>
</div>
</div>
<style>
.we-acc{
display:flex;flex-direction:column;gap:12px;
max-width:52rem;margin:0 auto;padding:0 20px;
}
/* Cards with a gap — not rows sharing hairline dividers. */
.we-acc-item{
background:var(--bg2);
border:1px solid var(--border);
border-radius:var(--radius-lg);
transition:border-color .2s var(--ease);
}
.we-acc-item:has(.we-acc-trigger[aria-expanded="true"]){border-color:var(--accent-b)}
.we-acc-item h3{margin:0;font:inherit}
.we-acc-trigger{
display:flex;align-items:center;justify-content:space-between;gap:20px;
width:100%;
/* A button carrying a card row must have its default padding reset, or the
UA padding shows up as an unexplained inset. */
padding:22px 24px;margin:0;
background:none;border:0;border-radius:var(--radius-lg);
font-family:var(--font-display);font-size:16.5px;font-weight:600;
letter-spacing:-.015em;line-height:1.35;
color:var(--text);text-align:left;cursor:pointer;
}
.we-acc-mark{
display:grid;place-items:center;width:32px;height:32px;flex:0 0 32px;
border-radius:50%;background:var(--bg3);border:1px solid var(--border);
color:var(--accent-text);
transition:transform .2s var(--ease), background .2s var(--ease);
}
.we-acc-mark svg{width:16px;height:16px}
.we-acc-trigger[aria-expanded="true"] .we-acc-mark{
transform:rotate(45deg);background:var(--accent-s);
}
/* 0fr -> 1fr animates to the panel's real height without measuring it in JS. */
.we-acc-panel{
display:grid;grid-template-rows:0fr;
transition:grid-template-rows .3s var(--ease);
}
.we-acc-panel.is-open{grid-template-rows:1fr}
.we-acc-inner{overflow:hidden}
.we-acc-inner p{
margin:0;padding:0 24px 24px;
font-family:var(--font-body);font-size:15px;line-height:1.75;
color:var(--text2);max-width:62ch;
}
.we-acc-trigger:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
.we-acc-panel,.we-acc-mark,.we-acc-item{transition:none}
}
</style>
<script>
(function () {
document.querySelectorAll('.we-acc-trigger').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = document.getElementById(btn.getAttribute('aria-controls'));
var open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
if (open) {
panel.classList.remove('is-open');
// hidden goes back on after the collapse, or the panel snaps shut.
panel.addEventListener('transitionend', function h() {
if (btn.getAttribute('aria-expanded') === 'false') panel.hidden = true;
panel.removeEventListener('transitionend', h);
});
} else {
panel.hidden = false;
requestAnimationFrame(function () { panel.classList.add('is-open'); });
}
});
});
})();
</script>
Needs tokens.css on the page.