25 Tailwind CSS FAQ Accordion
13 / 25

All 25 in 25 Tailwind CSS FAQ Accordion

  1. 01Tailwind Peer-Checked FAQ Accordion with No JavaScript
  2. 02Tailwind Native Details/Summary FAQ with Custom Styled Icons
  3. 03Tailwind Exclusive Accordion — Only One FAQ Open at a Time
  4. 04Tailwind Multi-Open FAQ Accordion with a Live CSS Counter
  5. 05Tailwind CSS FAQ Accordion Template (Free, Copy-Paste)
  6. 06Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
  7. 07Tailwind Glassmorphism FAQ Accordion with Frosted Backdrop Blur
  8. 08Tailwind Minimalist E-Commerce FAQ Accordion for Product Pages
  9. 09Tailwind Neumorphic Soft-UI FAQ Accordion Cards
  10. 10Tailwind Animate Height Auto FAQ with grid-rows 0fr to 1fr
  11. 11Tailwind FAQ Chevron Rotate 180 Degrees on Toggle
  12. 12Tailwind Animated Plus to Minus Icon Morph FAQ Accordion
  13. 13Tailwind Spring Bounce FAQ Accordion with linear() Easing
  14. 14Tailwind Two-Column FAQ Accordion Grid with Independent Columns
  15. 15Tailwind Horizontal FAQ Accordion with Sideways Expanding Panels
  16. 16Tailwind Categorised FAQ Help Centre with a Sticky Sidebar Menu
  17. 17Tailwind Card-Style FAQ Panels with a Layered Shadow Elevation System
  18. 18Tailwind FAQ Accordion with a Live Search Bar Filter
  19. 19Tailwind Expand All and Collapse All FAQ Accordion Buttons
  20. 20Tailwind FAQ with a Still Have Questions Contact CTA Card
  21. 21Tailwind Accessible ARIA FAQ Accordion with Keyboard Navigation
  22. 22Tailwind FAQ with FAQPage JSON-LD Schema for Rich Snippets and AI Search
  23. 23Bootstrap 5 Accordion Rebuilt in Tailwind (and the JavaScript You Delete)
  24. 24Tailwind FAQ Component in React, Vue, Svelte and Astro (One Port Recipe)
  25. 25Tailwind FAQ with Voice Search Using the Speech Recognition API
Tailwind v4MIT licensed

Tailwind Spring Bounce FAQ Accordion with linear() Easing

Spring physics without a physics engine: linear() plays back a real damped-spring curve — overshoot, wobble, settle — and in v4 it becomes a utility the moment you name it --ease-tfa13-spring in @theme. Panels bounce open like they have mass, the icon over-rotates and recoils, and closing stays deliberately calm.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="tfa-13 w-full min-h-svh grid place-items-center bg-tfa13-bg font-tfa13-sans text-tfa13-ink px-5 py-12 bg-[radial-gradient(800px_500px_at_90%_10%,var(--color-tfa13-glow),transparent_60%)]">
  <div class="w-[min(680px,100%)]">
    <header>
      <span class="inline-block rounded-full bg-tfa13-tint px-3.5 py-1.5 font-tfa13-mono text-[11.5px] font-semibold text-tfa13-acc">ease-tfa13-spring · linear(…)</span>
      <h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Panels with mass</h1>
      <p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa13-mut">A real damped-spring curve, sampled into CSS and named as a utility. Open = bounce. Close = calm, on purpose.</p>
    </header>

    <div class="mt-8 flex flex-col gap-2.5">
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
        <label for="tfa13-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">What makes this a “spring”, not just easing?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">The curve crosses its target more than once. This panel opens about 16% past its height, recoils, overshoots a little again, and settles — three crossings. A cubic-bezier manages one. That multi-crossing wobble is what your brain reads as mass and tension.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">Why does it not bounce when closing?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Deliberate asymmetry. A wobbly close feels like the UI resisting you. The base classes carry a calm 350ms ease-out, and <code class="rounded bg-tfa13-tint px-1.5 py-0.5 font-tfa13-mono text-[0.88em] text-tfa13-acc">peer-checked:</code> overrides duration and easing for the open direction only.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">How do I make my own curve?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Use a linear() generator: set mass, stiffness and damping, and it exports the sampled point list. Damping near 0.4 is cartoon, 0.55 (this demo) is playful-professional, 0.8 is barely-there luxury. Then name it in <code class="rounded bg-tfa13-tint px-1.5 py-0.5 font-tfa13-mono text-[0.88em] text-tfa13-acc">@theme</code> and it is a utility.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">Where should springs never be used?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Anywhere a user is about to click: the overshoot moves buttons under the cursor. And under reduced motion — a spring is precisely the vestibular trigger that setting exists for, so both directions drop to no transition.</p></div>
        </div>
      </div>
    </div>

    <p class="mt-6 text-[12.5px] text-tfa13-mut">Open: 800ms sampled spring · Close: 350ms ease-out · Fallback: one-overshoot bezier.</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tfa13-bg: #fbf5ef;
  --color-tfa13-bg: oklch(0.97 0.015 60);
  --color-tfa13-glow: #f6e0cf;
  --color-tfa13-glow: oklch(0.93 0.05 45 / 0.7);
  --color-tfa13-ink: #3a2a22;
  --color-tfa13-ink: oklch(0.25 0.03 40);
  --color-tfa13-mut: #6e5a50;
  --color-tfa13-mut: oklch(0.47 0.03 45);
  --color-tfa13-edge: #e8ddd2;
  --color-tfa13-edge: oklch(0.89 0.02 55);
  --color-tfa13-accedge: #dd9a7a;
  --color-tfa13-accedge: oklch(0.75 0.09 40);
  --color-tfa13-tint: #fbe7dc;
  --color-tfa13-tint: oklch(0.93 0.035 45);
  --color-tfa13-acc: #b5471f;
  --color-tfa13-acc: oklch(0.55 0.19 35);
  --font-tfa13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa13-mono: 'JetBrains Mono', ui-monospace, monospace;
  /* The whole point of this demo: an easing entry in @theme mints ease-tfa13-spring. */
  --ease-tfa13-spring: linear(0,0.009,0.035 2.1%,0.141 4.4%,0.723 12.9%,0.938,1.077 20.4%,1.121,1.149 24.3%,1.159,1.163 27%,1.154,1.129 32.8%,1.051 39.6%,1.017 43.1%,0.991,0.977 51%,0.974 53.8%,0.975 57.1%,0.997 69.8%,1.003 76.9%,1);
}

.tfa-13 {
  width: 100%;
  min-height: 100svh;
}
/* Single-overshoot fallback where linear() is unsupported — graceful, not broken. */

@supports not (transition-timing-function: linear(0, 1)) {
  .tfa-13 .peer:checked ~ * {
    transition-timing-function: cubic-bezier(.34,1.56,.64,1) !important;
    transition-duration: .5s !important;
  }
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tfa13-bg: #fbf5ef;
  --color-tfa13-bg: oklch(0.97 0.015 60);
  --color-tfa13-glow: #f6e0cf;
  --color-tfa13-glow: oklch(0.93 0.05 45 / 0.7);
  --color-tfa13-ink: #3a2a22;
  --color-tfa13-ink: oklch(0.25 0.03 40);
  --color-tfa13-mut: #6e5a50;
  --color-tfa13-mut: oklch(0.47 0.03 45);
  --color-tfa13-edge: #e8ddd2;
  --color-tfa13-edge: oklch(0.89 0.02 55);
  --color-tfa13-accedge: #dd9a7a;
  --color-tfa13-accedge: oklch(0.75 0.09 40);
  --color-tfa13-tint: #fbe7dc;
  --color-tfa13-tint: oklch(0.93 0.035 45);
  --color-tfa13-acc: #b5471f;
  --color-tfa13-acc: oklch(0.55 0.19 35);
  --font-tfa13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa13-mono: 'JetBrains Mono', ui-monospace, monospace;
  /* The whole point of this demo: an easing entry in @theme mints ease-tfa13-spring. */
  --ease-tfa13-spring: linear(0,0.009,0.035 2.1%,0.141 4.4%,0.723 12.9%,0.938,1.077 20.4%,1.121,1.149 24.3%,1.159,1.163 27%,1.154,1.129 32.8%,1.051 39.6%,1.017 43.1%,0.991,0.977 51%,0.974 53.8%,0.975 57.1%,0.997 69.8%,1.003 76.9%,1);
}

.tfa-13 {
  width: 100%;
  min-height: 100svh;
}
/* Single-overshoot fallback where linear() is unsupported — graceful, not broken. */

@supports not (transition-timing-function: linear(0, 1)) {
  .tfa-13 .peer:checked ~ * {
    transition-timing-function: cubic-bezier(.34,1.56,.64,1) !important;
    transition-duration: .5s !important;
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • --ease-tfa13-spring in @theme is the headline: a 23-point linear() curve becomes the utility ease-tfa13-spring, so the spring is nameable, reusable and composable with peer-checked:. The CSS version could only reference it through var() inside hand-written transition declarations.
  • Asymmetric transitions were the original's most easily-broken idea (declare the spring on the checked state, the calm close on the base, and hope the next dev notices). As duration-350 ease-out peer-checked:duration-800 peer-checked:ease-tfa13-spring both directions are visible in one place.
  • The @supports not (…linear(0,1)) fallback stayed in CSS and uses !important deliberately: it has to beat utility specificity for one legacy branch, and that is the honest trade for four lines.
  • UI upgrade from the CSS original: the accent moved oklch(0.62 0.19 35) → 0.55 and muted copy 0.5 → 0.47; the original chip and hover ink sat near 3.4:1 on the warm ground.
  • UI upgrade: open items lift with a tinted shadow, so the spring lands on something — a bounce with no depth change reads as a glitch on the first play.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Spring Bounce FAQ Accordion with linear() Easing
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-spring-bounce-faq-accordion-linear-easing/

Spring physics without a physics engine: linear() plays back a real damped-spring curve — overshoot, wobble, settle — and in v4 it becomes a utility the moment you name it --ease-tfa13-spring in @theme. Panels bounce open like they have mass, the icon over-rotates and recoils, and closing stays deliberately calm.
## HTML
```html
<section class="tfa-13 w-full min-h-svh grid place-items-center bg-tfa13-bg font-tfa13-sans text-tfa13-ink px-5 py-12 bg-[radial-gradient(800px_500px_at_90%_10%,var(--color-tfa13-glow),transparent_60%)]">
  <div class="w-[min(680px,100%)]">
    <header>
      <span class="inline-block rounded-full bg-tfa13-tint px-3.5 py-1.5 font-tfa13-mono text-[11.5px] font-semibold text-tfa13-acc">ease-tfa13-spring · linear(…)</span>
      <h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Panels with mass</h1>
      <p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa13-mut">A real damped-spring curve, sampled into CSS and named as a utility. Open = bounce. Close = calm, on purpose.</p>
    </header>

    <div class="mt-8 flex flex-col gap-2.5">
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
        <label for="tfa13-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">What makes this a “spring”, not just easing?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">The curve crosses its target more than once. This panel opens about 16% past its height, recoils, overshoots a little again, and settles — three crossings. A cubic-bezier manages one. That multi-crossing wobble is what your brain reads as mass and tension.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">Why does it not bounce when closing?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Deliberate asymmetry. A wobbly close feels like the UI resisting you. The base classes carry a calm 350ms ease-out, and <code class="rounded bg-tfa13-tint px-1.5 py-0.5 font-tfa13-mono text-[0.88em] text-tfa13-acc">peer-checked:</code> overrides duration and easing for the open direction only.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">How do I make my own curve?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Use a linear() generator: set mass, stiffness and damping, and it exports the sampled point list. Damping near 0.4 is cartoon, 0.55 (this demo) is playful-professional, 0.8 is barely-there luxury. Then name it in <code class="rounded bg-tfa13-tint px-1.5 py-0.5 font-tfa13-mono text-[0.88em] text-tfa13-acc">@theme</code> and it is a utility.</p></div>
        </div>
      </div>
      <div class="relative overflow-hidden rounded-[15px] border border-tfa13-edge bg-white shadow-[0_3px_12px_oklch(0.4_0.06_40/0.06)] transition-[border-color,box-shadow] duration-300 has-checked:border-tfa13-accedge has-checked:shadow-[0_16px_36px_-22px_oklch(0.62_0.19_35/0.4)]">
        <input id="tfa13-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
        <label for="tfa13-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-[15px] px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa13-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa13-acc">Where should springs never be used?
          <i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center rounded-lg bg-tfa13-tint before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa13-acc before:transition-[rotate] before:duration-350 before:ease-out after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa13-acc after:rotate-90 after:transition-[rotate,scale] after:duration-350 after:ease-out peer-checked:before:rotate-180 peer-checked:before:duration-800 peer-checked:before:ease-tfa13-spring peer-checked:after:rotate-180 peer-checked:after:scale-x-0 peer-checked:after:duration-800 peer-checked:after:ease-tfa13-spring motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
        </label>
        <div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-350 ease-out peer-checked:grid-rows-[1fr] peer-checked:duration-800 peer-checked:ease-tfa13-spring motion-reduce:transition-none motion-reduce:peer-checked:transition-none">
          <div class="min-h-0 overflow-hidden"><p class="max-w-[60ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa13-mut">Anywhere a user is about to click: the overshoot moves buttons under the cursor. And under reduced motion — a spring is precisely the vestibular trigger that setting exists for, so both directions drop to no transition.</p></div>
        </div>
      </div>
    </div>

    <p class="mt-6 text-[12.5px] text-tfa13-mut">Open: 800ms sampled spring · Close: 350ms ease-out · Fallback: one-overshoot bezier.</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tfa13-bg: #fbf5ef;
  --color-tfa13-bg: oklch(0.97 0.015 60);
  --color-tfa13-glow: #f6e0cf;
  --color-tfa13-glow: oklch(0.93 0.05 45 / 0.7);
  --color-tfa13-ink: #3a2a22;
  --color-tfa13-ink: oklch(0.25 0.03 40);
  --color-tfa13-mut: #6e5a50;
  --color-tfa13-mut: oklch(0.47 0.03 45);
  --color-tfa13-edge: #e8ddd2;
  --color-tfa13-edge: oklch(0.89 0.02 55);
  --color-tfa13-accedge: #dd9a7a;
  --color-tfa13-accedge: oklch(0.75 0.09 40);
  --color-tfa13-tint: #fbe7dc;
  --color-tfa13-tint: oklch(0.93 0.035 45);
  --color-tfa13-acc: #b5471f;
  --color-tfa13-acc: oklch(0.55 0.19 35);
  --font-tfa13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa13-mono: 'JetBrains Mono', ui-monospace, monospace;
  /* The whole point of this demo: an easing entry in @theme mints ease-tfa13-spring. */
  --ease-tfa13-spring: linear(0,0.009,0.035 2.1%,0.141 4.4%,0.723 12.9%,0.938,1.077 20.4%,1.121,1.149 24.3%,1.159,1.163 27%,1.154,1.129 32.8%,1.051 39.6%,1.017 43.1%,0.991,0.977 51%,0.974 53.8%,0.975 57.1%,0.997 69.8%,1.003 76.9%,1);
}

.tfa-13 {
  width: 100%;
  min-height: 100svh;
}
/* Single-overshoot fallback where linear() is unsupported — graceful, not broken. */

@supports not (transition-timing-function: linear(0, 1)) {
  .tfa-13 .peer:checked ~ * {
    transition-timing-function: cubic-bezier(.34,1.56,.64,1) !important;
    transition-duration: .5s !important;
  }
}
```

How this works

Why linear(). A cubic-bezier is one hump — it can overshoot once but cannot wobble. linear() plays back any curve as a list of sampled points, which is how a damped spring gets smuggled into pure CSS. Values above 1 are the overshoot: this panel opens about 16% too tall, recoils, overshoots slightly again, then settles — a damping ratio near 0.55. You generate the point list from spring parameters rather than writing it by hand.

The port's win is naming it. In CSS the curve was a --spring custom property referenced in four transition declarations. In v4 the same curve in @theme as --ease-tfa13-spring becomes the utility ease-tfa13-spring, usable anywhere, composable with any variant — which is what makes the asymmetry below readable at all.

Asymmetric transitions, stated locally. Two craft rules make springs feel premium rather than clownish: open springy, close calm (a closing panel that wobbles feels like it is fighting you), and one spring per interaction. The base classes carry the calm close; peer-checked: overrides duration and easing for the open direction. In CSS this meant re-declaring transition in both states and hoping the next reader noticed — here the two directions sit side by side in one class list.

Make it yours

  • Bounciness: regenerate linear() with damping 0.4 (cartoonier) to 0.8 (barely perceptible); this is about 0.55.
  • Entrance too: the same easing on a translate/scale with @starting-style makes cards land on page load.
  • Duration: springs need runway. Under 500ms the wobble aliases into jitter; 800ms suits this curve.
  • Calm everything: delete the two peer-checked:ease-*/duration-800 pairs and the accordion is an ordinary ease-out panel.

Gotchas — read before shipping

  • Springs must not move click targets: never spring a panel containing form fields the user is about to click. Content panels only.
  • A linear() list with too few samples renders visibly segmented — real exports carry 20+ points.
  • Reduced motion asks for the spring to go away entirely, not to run slower. motion-reduce:transition-none on both directions is the honest answer.
  • Put the spring on the OPEN state only. Declaring it on the base makes closing bounce too, which is the most common version of this mistake.

Browser support

ChromeSafariFirefoxEdge
113+17.2+112+113+

linear() easing shipped across engines in 2023. The @supports fallback gives a single-overshoot bezier back to 2021, and the accordion works regardless.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…