10 / 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 Animate Height Auto FAQ with grid-rows 0fr to 1fr
The technique demo: how grid-template-rows: 0fr → 1fr finally solved CSS's oldest animation problem — transitioning to height:auto — written entirely in utilities. This FAQ dissects its own animation: every open panel carries a live annotation of what the track is doing, and the answers explain why max-height hacks break and why the one overflow-hidden wrapper is mandatory.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
The code
<section class="tfa-10 w-full min-h-svh grid place-items-center bg-tfa10-bg font-tfa10-sans text-tfa10-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_100%_100%,var(--color-tfa10-glow),transparent_58%)]">
<div class="w-[min(700px,100%)]">
<header>
<span class="inline-block rounded-full bg-tfa10-tint px-3.5 py-1.5 font-tfa10-mono text-[12px] font-semibold text-tfa10-acc">grid-rows-[0fr] → grid-rows-[1fr]</span>
<h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Animating to height: auto</h1>
<p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa10-mut">The accordion that explains its own animation. Watch the track annotation as panels open.</p>
</header>
<div class="mt-8 flex flex-col gap-2.5">
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
<label for="tfa10-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why doesn't height: auto animate?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Because <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">auto</code> is a keyword, not a number — the engine has nothing to interpolate between 0px and “whatever it turns out to be”. Fraction units sidestep this: <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">0fr → 1fr</code> is a numeric share of the content's size, resolved to real pixels every frame.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">grid-template-rows: <b class='font-bold'>1fr</b> · height resolved per frame</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">What's wrong with max-height: 500px?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">It animates to 500px, not to your content. Short answers finish early then “wait” invisibly; long answers get clipped. Every max-height accordion has a magic number that is wrong for someone's content, font size, or translation.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">this panel needed no magic number</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why the extra inner wrapper?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Grid items refuse to shrink below <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-content</code> by default — a 0fr track with an unclippable child still renders tall. <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">overflow-hidden</code> plus <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-h-0</code> on the direct child releases that constraint. Forget it and the accordion “never closes”.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">class="min-h-0 overflow-hidden"</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Does this work with really long content?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Yes — that is the whole point. This answer is deliberately longer to prove it: the track interpolates shares, not pixels, so a two-line answer and a twelve-line answer both animate over the same 500 milliseconds with the same easing, and the browser resolves the actual height continuously during the transition. Resize the pane mid-animation; it self-corrects. Add another paragraph tomorrow; there is nothing to update. That is what “no magic numbers” buys you in maintenance terms.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">same duration-500 regardless of content length</span>
</div>
</div>
</div>
</div>
<p class="mt-6 text-[12.5px] text-tfa10-mut">The same pattern rotated 90° is demo 15's horizontal accordion (<code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">grid-cols-[0fr]</code>).</p>
</div>
</section><section class="tfa-10 w-full min-h-svh grid place-items-center bg-tfa10-bg font-tfa10-sans text-tfa10-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_100%_100%,var(--color-tfa10-glow),transparent_58%)]">
<div class="w-[min(700px,100%)]">
<header>
<span class="inline-block rounded-full bg-tfa10-tint px-3.5 py-1.5 font-tfa10-mono text-[12px] font-semibold text-tfa10-acc">grid-rows-[0fr] → grid-rows-[1fr]</span>
<h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Animating to height: auto</h1>
<p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa10-mut">The accordion that explains its own animation. Watch the track annotation as panels open.</p>
</header>
<div class="mt-8 flex flex-col gap-2.5">
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
<label for="tfa10-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why doesn't height: auto animate?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Because <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">auto</code> is a keyword, not a number — the engine has nothing to interpolate between 0px and “whatever it turns out to be”. Fraction units sidestep this: <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">0fr → 1fr</code> is a numeric share of the content's size, resolved to real pixels every frame.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">grid-template-rows: <b class='font-bold'>1fr</b> · height resolved per frame</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">What's wrong with max-height: 500px?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">It animates to 500px, not to your content. Short answers finish early then “wait” invisibly; long answers get clipped. Every max-height accordion has a magic number that is wrong for someone's content, font size, or translation.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">this panel needed no magic number</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why the extra inner wrapper?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Grid items refuse to shrink below <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-content</code> by default — a 0fr track with an unclippable child still renders tall. <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">overflow-hidden</code> plus <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-h-0</code> on the direct child releases that constraint. Forget it and the accordion “never closes”.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">class="min-h-0 overflow-hidden"</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Does this work with really long content?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Yes — that is the whole point. This answer is deliberately longer to prove it: the track interpolates shares, not pixels, so a two-line answer and a twelve-line answer both animate over the same 500 milliseconds with the same easing, and the browser resolves the actual height continuously during the transition. Resize the pane mid-animation; it self-corrects. Add another paragraph tomorrow; there is nothing to update. That is what “no magic numbers” buys you in maintenance terms.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">same duration-500 regardless of content length</span>
</div>
</div>
</div>
</div>
<p class="mt-6 text-[12.5px] text-tfa10-mut">The same pattern rotated 90° is demo 15's horizontal accordion (<code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">grid-cols-[0fr]</code>).</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}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-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}<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.
- Like demo 01, the stylesheet here is tokens only: the technique this demo exists to teach is entirely expressible in utilities, which is the strongest thing that can be said about porting accordions to Tailwind.
transition-[grid-template-rows]is the one place the arbitrary-property syntax is not optional — there is notransition-gridutility, andtransition-allwould animate the border and shadow on the panel's timing instead of their own.min-h-0 overflow-hiddenin one class list makes the belt-and-braces intent obvious to the next reader, where the CSS original had the two declarations in a rule you had to go find.- UI upgrade from the CSS original: muted copy moved
oklch(0.67)→0.79and the panel0.21→0.24, taking the annotation text from about 3.2:1 to 5.4:1 — the annotations are the content in this demo, so they had to be the most legible thing on screen. - UI upgrade: open items now lift with a cyan-tinted shadow instead of only tinting their border, and the track annotation got a solid tinted ground rather than a 7% mix that was effectively invisible on the dark panel.
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 Animate Height Auto FAQ with grid-rows 0fr to 1fr
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-animate-height-auto-faq-grid-rows/
The technique demo: how grid-template-rows: 0fr → 1fr finally solved CSS's oldest animation problem — transitioning to height:auto — written entirely in utilities. This FAQ dissects its own animation: every open panel carries a live annotation of what the track is doing, and the answers explain why max-height hacks break and why the one overflow-hidden wrapper is mandatory.
## HTML
```html
<section class="tfa-10 w-full min-h-svh grid place-items-center bg-tfa10-bg font-tfa10-sans text-tfa10-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_100%_100%,var(--color-tfa10-glow),transparent_58%)]">
<div class="w-[min(700px,100%)]">
<header>
<span class="inline-block rounded-full bg-tfa10-tint px-3.5 py-1.5 font-tfa10-mono text-[12px] font-semibold text-tfa10-acc">grid-rows-[0fr] → grid-rows-[1fr]</span>
<h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Animating to height: auto</h1>
<p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa10-mut">The accordion that explains its own animation. Watch the track annotation as panels open.</p>
</header>
<div class="mt-8 flex flex-col gap-2.5">
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
<label for="tfa10-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why doesn't height: auto animate?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Because <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">auto</code> is a keyword, not a number — the engine has nothing to interpolate between 0px and “whatever it turns out to be”. Fraction units sidestep this: <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">0fr → 1fr</code> is a numeric share of the content's size, resolved to real pixels every frame.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">grid-template-rows: <b class='font-bold'>1fr</b> · height resolved per frame</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">What's wrong with max-height: 500px?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">It animates to 500px, not to your content. Short answers finish early then “wait” invisibly; long answers get clipped. Every max-height accordion has a magic number that is wrong for someone's content, font size, or translation.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">this panel needed no magic number</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why the extra inner wrapper?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Grid items refuse to shrink below <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-content</code> by default — a 0fr track with an unclippable child still renders tall. <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">overflow-hidden</code> plus <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-h-0</code> on the direct child releases that constraint. Forget it and the accordion “never closes”.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">class="min-h-0 overflow-hidden"</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Does this work with really long content?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Yes — that is the whole point. This answer is deliberately longer to prove it: the track interpolates shares, not pixels, so a two-line answer and a twelve-line answer both animate over the same 500 milliseconds with the same easing, and the browser resolves the actual height continuously during the transition. Resize the pane mid-animation; it self-corrects. Add another paragraph tomorrow; there is nothing to update. That is what “no magic numbers” buys you in maintenance terms.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">same duration-500 regardless of content length</span>
</div>
</div>
</div>
</div>
<p class="mt-6 text-[12.5px] text-tfa10-mut">The same pattern rotated 90° is demo 15's horizontal accordion (<code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">grid-cols-[0fr]</code>).</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}
```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 Animate Height Auto FAQ with grid-rows 0fr to 1fr
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-animate-height-auto-faq-grid-rows/
The technique demo: how grid-template-rows: 0fr → 1fr finally solved CSS's oldest animation problem — transitioning to height:auto — written entirely in utilities. This FAQ dissects its own animation: every open panel carries a live annotation of what the track is doing, and the answers explain why max-height hacks break and why the one overflow-hidden wrapper is mandatory.
## HTML
```html
<section class="tfa-10 w-full min-h-svh grid place-items-center bg-tfa10-bg font-tfa10-sans text-tfa10-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_100%_100%,var(--color-tfa10-glow),transparent_58%)]">
<div class="w-[min(700px,100%)]">
<header>
<span class="inline-block rounded-full bg-tfa10-tint px-3.5 py-1.5 font-tfa10-mono text-[12px] font-semibold text-tfa10-acc">grid-rows-[0fr] → grid-rows-[1fr]</span>
<h1 class="mt-4 text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Animating to height: auto</h1>
<p class="mt-3 max-w-[54ch] text-[15px] leading-relaxed text-tfa10-mut">The accordion that explains its own animation. Watch the track annotation as panels open.</p>
</header>
<div class="mt-8 flex flex-col gap-2.5">
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q1" type="checkbox" class="peer absolute opacity-0 pointer-events-none" checked>
<label for="tfa10-q1" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why doesn't height: auto animate?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Because <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">auto</code> is a keyword, not a number — the engine has nothing to interpolate between 0px and “whatever it turns out to be”. Fraction units sidestep this: <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">0fr → 1fr</code> is a numeric share of the content's size, resolved to real pixels every frame.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">grid-template-rows: <b class='font-bold'>1fr</b> · height resolved per frame</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q2" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q2" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">What's wrong with max-height: 500px?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">It animates to 500px, not to your content. Short answers finish early then “wait” invisibly; long answers get clipped. Every max-height accordion has a magic number that is wrong for someone's content, font size, or translation.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">this panel needed no magic number</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q3" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q3" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Why the extra inner wrapper?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Grid items refuse to shrink below <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-content</code> by default — a 0fr track with an unclippable child still renders tall. <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">overflow-hidden</code> plus <code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">min-h-0</code> on the direct child releases that constraint. Forget it and the accordion “never closes”.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">class="min-h-0 overflow-hidden"</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl border border-tfa10-edge bg-tfa10-panel transition-[border-color,box-shadow] duration-300 has-checked:border-tfa10-accedge has-checked:shadow-[0_18px_40px_-26px_oklch(0.75_0.14_190/0.5)]">
<input id="tfa10-q4" type="checkbox" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa10-q4" class="flex min-h-11 cursor-pointer select-none items-center justify-between gap-4 rounded-2xl px-5 py-4 text-[15.5px] font-semibold tracking-[-0.01em] transition-colors duration-200 hover:text-tfa10-acc peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-tfa10-acc">Does this work with really long content?
<i aria-hidden="true" class="relative grid size-6 shrink-0 place-items-center before:absolute before:h-0.5 before:w-3 before:rounded-full before:bg-tfa10-acc before:transition-transform before:duration-500 before:ease-[cubic-bezier(0.4,0,0.2,1)] after:absolute after:h-0.5 after:w-3 after:rounded-full after:bg-tfa10-acc after:rotate-90 after:transition-transform after:duration-500 after:ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:before:rotate-180 peer-checked:after:rotate-0 motion-reduce:before:transition-none motion-reduce:after:transition-none"></i>
</label>
<div class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] peer-checked:grid-rows-[1fr] motion-reduce:transition-none">
<div class="min-h-0 overflow-hidden">
<p class="max-w-[62ch] px-5 text-[14px] leading-[1.7] text-tfa10-mut">Yes — that is the whole point. This answer is deliberately longer to prove it: the track interpolates shares, not pixels, so a two-line answer and a twelve-line answer both animate over the same 500 milliseconds with the same easing, and the browser resolves the actual height continuously during the transition. Resize the pane mid-animation; it self-corrects. Add another paragraph tomorrow; there is nothing to update. That is what “no magic numbers” buys you in maintenance terms.</p>
<span aria-hidden="true" class="mx-5 mb-4 mt-3 block rounded-r-lg border-l-[3px] border-tfa10-acc bg-tfa10-tint px-3 py-2 font-tfa10-mono text-[11.5px] text-tfa10-acc">same duration-500 regardless of content length</span>
</div>
</div>
</div>
</div>
<p class="mt-6 text-[12.5px] text-tfa10-mut">The same pattern rotated 90° is demo 15's horizontal accordion (<code class="rounded bg-tfa10-tint px-1.5 py-0.5 font-tfa10-mono text-[0.88em] text-tfa10-acc">grid-cols-[0fr]</code>).</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfa10-bg: #1a1f24;
--color-tfa10-bg: oklch(0.2 0.015 220);
--color-tfa10-panel: #212a30;
--color-tfa10-panel: oklch(0.24 0.02 220);
--color-tfa10-glow: #1c3b41;
--color-tfa10-glow: oklch(0.28 0.06 190 / 0.5);
--color-tfa10-ink: #f1f5f6;
--color-tfa10-ink: oklch(0.96 0.008 210);
--color-tfa10-mut: #b4bfc3;
--color-tfa10-mut: oklch(0.79 0.014 215);
--color-tfa10-edge: #38434a;
--color-tfa10-edge: oklch(0.34 0.022 218);
--color-tfa10-accedge: #4e8f96;
--color-tfa10-accedge: oklch(0.6 0.08 190);
--color-tfa10-tint: #24383c;
--color-tfa10-tint: oklch(0.29 0.04 190);
--color-tfa10-acc: #7fdbe4;
--color-tfa10-acc: oklch(0.85 0.12 190);
--font-tfa10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa10-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfa-10 {
width: 100%;
min-height: 100svh;
}
```How this works
Why height:auto never animated. auto is a keyword, not a number — the engine has nothing to interpolate toward. Every workaround lied: max-height:500px animates to 500px (wrong speed, dead wait, or clipped content); JS measured scrollHeight and wrote inline pixels (layout thrash, resize bugs). Fractions reframe the question: the panel is a grid with a grid-rows-[0fr] track that becomes grid-rows-[1fr] under peer-checked:, with transition-[grid-template-rows] naming the property.
An fr is not a length; it is a share of free space, and shares interpolate. 0fr is a track entitled to 0% of the content's height, 1fr to 100%; halfway through the transition the track sits at 0.5fr and the browser resolves real pixels each frame. Content of any length animates at the same perceived speed, with no magic numbers.
The wrapper rule everyone forgets. Grid items carry an implicit min-height: min-content, so a 0fr track containing an unclippable child still renders full height. overflow-hidden (or min-h-0; this demo sets both) releases it. Rotate the whole thing and you get grid-cols-[0fr] → [1fr] — demo 15 is exactly that.
Porting note: transition-all animates the track too, but drags every other property onto the same timing and makes the panel feel cheap. Name the property.
Make it yours
- Easing: the standard curve is the default here. Try
ease-[cubic-bezier(0.2,0.7,0.2,1)]for a floatier settle, or demo 13'slinear()spring. - Fade + slide: add opacity and translate transitions on the inner wrapper, staggered about 80ms behind the row, for a layered feel.
- Any state source:
peer-checked:here,open:/group-open:on details, or adata-openattribute from JS — only the variant changes. - Content scent: closed state
grid-rows-[2.6em]instead of[0fr]shows the first line of every answer.
Gotchas — read before shipping
- THE bug: forgetting
overflow-hiddenon the direct child. The panel then never visually closes even though the track is0fr. - Padding on the animated row is outside the track math — it keeps the panel that much open when closed. Pad the inner wrapper, as here.
transition-allworks but is wrong: name the property so colour and shadow transitions keep their own timings.- Nested accordions work (the inner height change happens inside the outer
1frtrack), but give each its own overflow wrapper or the outer clips the inner's animation.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 113+ | 111+ |
Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 107+.
grid-template-rows interpolation: Firefox led in 2019, Chrome 107 and Safari 16 completed it in 2022 — safe unflagged. The practical floor is Tailwind v4's generated sheet.