Yes. Your plan runs to the end of the period you paid for, then stops.
Accordion
import { useId, useState } from 'react';
const FAQ = [
{ q: 'Can I cancel anytime?', a: 'Yes. Your plan runs to the end of the period you paid for, then stops.' },
{ q: 'Do you offer refunds?', a: 'Within 14 days of a charge, with no questions asked.' },
{ q: 'Is there a team plan?', a: 'Teams of three or more get shared libraries and one invoice.' },
];
const css = `
.card-accordion { box-sizing: border-box; width: 240px; padding: 6px 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-accordion__item + .card-accordion__item { border-top: 1px solid light-dark(#ebedf0, #232529); }
.card-accordion__q { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; height: 40px; padding: 0; border: 0; background: none; color: light-dark(#373a40, #f1f2f3); font: inherit; font-size: 12px; font-weight: 600; text-align: left; cursor: pointer; }
.card-accordion__q:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; border-radius: 4px; }
.card-accordion__icon { position: relative; flex: none; width: 12px; height: 12px; }
.card-accordion__icon::before, .card-accordion__icon::after { content: ''; position: absolute; left: 0; top: 5px; width: 12px; height: 2px; border-radius: 1px; background: light-dark(#50535b, #c6c8cd); transition: transform 0.35s cubic-bezier(0.3, 1.4, 0.5, 1); }
/* The vertical bar turns flat: plus becomes minus. */
.card-accordion__icon::after { transform: rotate(90deg); }
.card-accordion__q[aria-expanded='true'] .card-accordion__icon::after { transform: rotate(0deg); }
.card-accordion__q[aria-expanded='true'] .card-accordion__icon::before { transform: rotate(180deg); }
/* The answer opens to its natural height by animating its grid track from 0fr to 1fr. */
.card-accordion__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.card-accordion__panel--open { grid-template-rows: 1fr; }
.card-accordion__panel > div { min-height: 0; overflow: hidden; }
.card-accordion__a { margin: 0; padding: 0 0 12px; font-size: 11.5px; line-height: 1.45; color: light-dark(#50535b, #c6c8cd); opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.3s; }
.card-accordion__panel--open .card-accordion__a { opacity: 1; transform: none; transition-delay: 0.1s; }
@media (prefers-reduced-motion: reduce) {
.card-accordion__panel, .card-accordion__a, .card-accordion__icon::before, .card-accordion__icon::after { transition: none; }
}
`;
export function AccordionCard() {
const [open, setOpen] = useState<number | null>(0);
const id = useId();
return (
<>
<style>
{css}
</style>
<article className="card-accordion">
{FAQ.map((f, i) => (
<div key={f.q} className="card-accordion__item">
<button type="button" id={`${id}q${i}`} className="card-accordion__q" aria-expanded={open === i} aria-controls={`${id}a${i}`} onClick={() => setOpen((o) => (o === i ? null : i))}>
{f.q}
<span className="card-accordion__icon" aria-hidden="true" />
</button>
<div id={`${id}a${i}`} className={`card-accordion__panel${open === i ? ' card-accordion__panel--open' : ''}`} role="region" aria-labelledby={`${id}q${i}`}>
<div>
<p className="card-accordion__a" aria-hidden={open !== i}>
{f.a}
</p>
</div>
</div>
</div>
))}
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardAccordion(el) on the mounted element instead.
<article class="card-accordion">
<div class="card-accordion__item">
<button type="button" id="card-accordion-q0" class="card-accordion__q" aria-expanded="true" aria-controls="card-accordion-a0">
Can I cancel anytime?
<span class="card-accordion__icon" aria-hidden="true"></span>
</button>
<div id="card-accordion-a0" class="card-accordion__panel card-accordion__panel--open" role="region" aria-labelledby="card-accordion-q0">
<div>
<p class="card-accordion__a" aria-hidden="false">Yes. Your plan runs to the end of the period you paid for, then stops.</p>
</div>
</div>
</div>
<div class="card-accordion__item">
<button type="button" id="card-accordion-q1" class="card-accordion__q" aria-expanded="false" aria-controls="card-accordion-a1">
Do you offer refunds?
<span class="card-accordion__icon" aria-hidden="true"></span>
</button>
<div id="card-accordion-a1" class="card-accordion__panel" role="region" aria-labelledby="card-accordion-q1">
<div>
<p class="card-accordion__a" aria-hidden="true">Within 14 days of a charge, with no questions asked.</p>
</div>
</div>
</div>
<div class="card-accordion__item">
<button type="button" id="card-accordion-q2" class="card-accordion__q" aria-expanded="false" aria-controls="card-accordion-a2">
Is there a team plan?
<span class="card-accordion__icon" aria-hidden="true"></span>
</button>
<div id="card-accordion-a2" class="card-accordion__panel" role="region" aria-labelledby="card-accordion-q2">
<div>
<p class="card-accordion__a" aria-hidden="true">Teams of three or more get shared libraries and one invoice.</p>
</div>
</div>
</div>
</article>.card-accordion { box-sizing: border-box; width: 240px; padding: 6px 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-accordion__item + .card-accordion__item { border-top: 1px solid light-dark(#ebedf0, #232529); }
.card-accordion__q { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; height: 40px; padding: 0; border: 0; background: none; color: light-dark(#373a40, #f1f2f3); font: inherit; font-size: 12px; font-weight: 600; text-align: left; cursor: pointer; }
.card-accordion__q:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; border-radius: 4px; }
.card-accordion__icon { position: relative; flex: none; width: 12px; height: 12px; }
.card-accordion__icon::before, .card-accordion__icon::after { content: ''; position: absolute; left: 0; top: 5px; width: 12px; height: 2px; border-radius: 1px; background: light-dark(#50535b, #c6c8cd); transition: transform 0.35s cubic-bezier(0.3, 1.4, 0.5, 1); }
/* The vertical bar turns flat: plus becomes minus. */
.card-accordion__icon::after { transform: rotate(90deg); }
.card-accordion__q[aria-expanded='true'] .card-accordion__icon::after { transform: rotate(0deg); }
.card-accordion__q[aria-expanded='true'] .card-accordion__icon::before { transform: rotate(180deg); }
/* The answer opens to its natural height by animating its grid track from 0fr to 1fr. */
.card-accordion__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.card-accordion__panel--open { grid-template-rows: 1fr; }
.card-accordion__panel > div { min-height: 0; overflow: hidden; }
.card-accordion__a { margin: 0; padding: 0 0 12px; font-size: 11.5px; line-height: 1.45; color: light-dark(#50535b, #c6c8cd); opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.3s; }
.card-accordion__panel--open .card-accordion__a { opacity: 1; transform: none; transition-delay: 0.1s; }
@media (prefers-reduced-motion: reduce) {
.card-accordion__panel, .card-accordion__a, .card-accordion__icon::before, .card-accordion__icon::after { transition: none; }
}function initCardAccordion(root) {
var qs = root.querySelectorAll('.card-accordion__q');
var panels = root.querySelectorAll('.card-accordion__panel');
var open = 0;
// Opening one closes the other; each answer animates its grid track between 0fr and 1fr, so it opens to its natural height.
function onClick(e) {
var i = Array.prototype.indexOf.call(qs, e.currentTarget);
open = open === i ? null : i;
qs.forEach(function (q, k) {
q.setAttribute('aria-expanded', String(open === k));
panels[k].classList.toggle('card-accordion__panel--open', open === k);
panels[k].querySelector('.card-accordion__a').setAttribute('aria-hidden', String(open !== k));
});
}
qs.forEach(function (q) {
q.addEventListener('click', onClick);
});
return function () {
qs.forEach(function (q) {
q.removeEventListener('click', onClick);
});
};
}
document.querySelectorAll('.card-accordion').forEach(initCardAccordion);Goal: Create a "Accordion" card.
Visual behavior: An FAQ card: open a question and its answer slides open to its natural height as the plus turns into a minus, closing the one that was open.
Technique:
- Each answer sits in a grid whose single track animates from 0fr to 1fr, so it opens to its natural height without measuring.
- The plus is a ::before and an ::after bar; opening turns the vertical one flat and spins the other half a turn, so it reads as one turn into a minus.
- The answer text fades and drops into place a beat after the panel starts opening.
Accessibility: Each question is a button with aria-expanded and aria-controls, and each answer is a region labelled by its question. Under prefers-reduced-motion the answers open at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Accordion" card.
Visual behavior: An FAQ card: open a question and its answer slides open to its natural height as the plus turns into a minus, closing the one that was open.
Technique:
- Each answer sits in a grid whose single track animates from 0fr to 1fr, so it opens to its natural height without measuring.
- The plus is a ::before and an ::after bar; opening turns the vertical one flat and spins the other half a turn, so it reads as one turn into a minus.
- The answer text fades and drops into place a beat after the panel starts opening.
Accessibility: Each question is a button with aria-expanded and aria-controls, and each answer is a region labelled by its question. Under prefers-reduced-motion the answers open at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
An FAQ card: open a question and its answer slides open to its natural height as the plus turns into a minus, closing the one that was open.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- click, state change, card
- Techniques
- css transition, pseudo elements, transform only
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Each question is a button with aria-expanded and aria-controls, and each answer is a region labelled by its question. Under prefers-reduced-motion the answers open at once.