13 / 25
All 25 in 25 Tailwind CSS FAQ Accordion
- 01Tailwind Peer-Checked FAQ Accordion with No JavaScript
- 02Tailwind Native Details/Summary FAQ with Custom Styled Icons
- 03Tailwind Exclusive Accordion — Only One FAQ Open at a Time
- 04Tailwind Multi-Open FAQ Accordion with a Live CSS Counter
- 05Tailwind CSS FAQ Accordion Template (Free, Copy-Paste)
- 06Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
- 07Tailwind Glassmorphism FAQ Accordion with Frosted Backdrop Blur
- 08Tailwind Minimalist E-Commerce FAQ Accordion for Product Pages
- 09Tailwind Neumorphic Soft-UI FAQ Accordion Cards
- 10Tailwind Animate Height Auto FAQ with grid-rows 0fr to 1fr
- 11Tailwind FAQ Chevron Rotate 180 Degrees on Toggle
- 12Tailwind Animated Plus to Minus Icon Morph FAQ Accordion
- 13Tailwind Spring Bounce FAQ Accordion with linear() Easing
- 14Tailwind Two-Column FAQ Accordion Grid with Independent Columns
- 15Tailwind Horizontal FAQ Accordion with Sideways Expanding Panels
- 16Tailwind Categorised FAQ Help Centre with a Sticky Sidebar Menu
- 17Tailwind Card-Style FAQ Panels with a Layered Shadow Elevation System
- 18Tailwind FAQ Accordion with a Live Search Bar Filter
- 19Tailwind Expand All and Collapse All FAQ Accordion Buttons
- 20Tailwind FAQ with a Still Have Questions Contact CTA Card
- 21Tailwind Accessible ARIA FAQ Accordion with Keyboard Navigation
- 22Tailwind FAQ with FAQPage JSON-LD Schema for Rich Snippets and AI Search
- 23Bootstrap 5 Accordion Rebuilt in Tailwind (and the JavaScript You Delete)
- 24Tailwind FAQ Component in React, Vue, Svelte and Astro (One Port Recipe)
- 25Tailwind FAQ with Voice Search Using the Speech Recognition API
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.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
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><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;
}
}@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.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@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;
}
}@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;
}
}<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">
<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-springin@themeis the headline: a 23-pointlinear()curve becomes the utilityease-tfa13-spring, so the spring is nameable, reusable and composable withpeer-checked:. The CSS version could only reference it throughvar()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-springboth directions are visible in one place. - The
@supports not (…linear(0,1))fallback stayed in CSS and uses!importantdeliberately: 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.55and muted copy0.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.
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;
}
}
```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/scalewith@starting-stylemakes 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-800pairs 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-noneon 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.