25 Tailwind CSS FAQ Accordion
25 / 25

All 25 in 25 Tailwind CSS FAQ Accordion

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

Tailwind FAQ with Voice Search Using the Speech Recognition API

Speak your question and watch the FAQ filter: a microphone button feeds the Speech Recognition API into the same live filter as demo 18, with interim results streaming into the input as you talk and a pulsing listening ring. The part most voice demos skip is the whole point here — the mic only renders where the API exists, the typed filter is the baseline everyone gets, and every state change is announced.

Live Demo
Plain CSSTailwind v4
Try it

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

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="tfa-25 w-full min-h-svh grid place-items-center bg-tfa25-bg font-tfa25-sans text-tfa25-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_50%_-15%,var(--color-tfa25-glow),transparent_60%)]">
  <div class="w-[min(680px,100%)]">
    <header>
      <h1 class="text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Ask out loud</h1>
      <p class="mt-3 max-w-[56ch] text-[15px] leading-relaxed text-tfa25-mut">Tap the mic and speak, or type — the filter is the same. Voice is the layer on top, never the requirement.</p>
    </header>

    <div class="mt-6 flex min-h-[54px] items-center gap-2.5 rounded-[14px] border border-tfa25-edge bg-tfa25-panel px-4 transition-[border-color,box-shadow] duration-250 focus-within:border-tfa25-acc focus-within:ring-3 focus-within:ring-tfa25-ring">
      <svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" class="shrink-0 text-tfa25-mut"><circle cx="9" cy="9" r="6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M13.5 13.5L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      <input id="tfa25-input" type="search" aria-label="Search or dictate a question" placeholder="Type, or press the mic…" class="min-h-11 w-full min-w-0 flex-1 border-0 bg-transparent text-[15px] text-tfa25-ink outline-none placeholder:text-tfa25-mut [&::-webkit-search-cancel-button]:appearance-none">
      <button type="button" id="tfa25-mic" aria-pressed="false" aria-label="Search by voice" class="relative grid size-11 shrink-0 cursor-pointer place-items-center rounded-full border border-tfa25-edge bg-transparent text-tfa25-acc transition-[background-color,border-color,color] duration-250 hover:border-tfa25-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tfa25-acc aria-pressed:border-tfa25-acc aria-pressed:bg-tfa25-acc aria-pressed:text-tfa25-bg">
        <span aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-full bg-tfa25-acc/40 opacity-0"></span>
        <svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" class="relative"><path d="M12 3.5a3 3 0 013 3v5a3 3 0 01-6 0v-5a3 3 0 013-3z" fill="currentColor"/><path d="M6.5 11a5.5 5.5 0 0011 0M12 16.5V20M9 20h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
      </button>
    </div>
    <p id="tfa25-status" aria-live="polite" class="mt-2 min-h-5 font-tfa25-mono text-[11.5px] text-tfa25-acc"></p>

    <div data-panel id="tfa25-list" class="mt-2 flex flex-col gap-2.5">
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]" open>
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">How do I reset my password?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Use the Forgot password link on the sign-in screen — the reset email arrives within a minute and the link is valid for one hour. Passkeys can be added afterwards from Security settings.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Can I change my plan later?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Any time, in either direction. Upgrades apply instantly and prorate; downgrades take effect at the end of the billing period so you keep what you paid for.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Do you support two-factor authentication?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Yes — authenticator apps and hardware keys on every plan, free, with enforced 2FA available for whole workspaces.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Where can I download invoices?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Billing → History, as PDFs with your tax details. Billing-only members can be added for your accountant.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Is there a mobile app?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Native apps for iOS and Android, plus an installable web app for everything else. Offline edits sync when you reconnect.</p>
        </details>
    </div>

    <div id="tfa25-empty" hidden class="mt-4 rounded-[14px] border border-dashed border-tfa25-edge px-5 py-6 text-center text-tfa25-mut">
      <b class="text-[15px] text-tfa25-ink">Nothing matched that.</b>
      <p class="mt-1.5 text-[13px]">Try fewer words — speech recognition guesses, and sometimes badly.</p>
    </div>

    <p class="mt-5 text-[12.5px] leading-relaxed text-tfa25-mut">Where the API is missing (Firefox today), the mic button is removed at runtime rather than shown broken.</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tfa25-bg: #1c1a26;
  --color-tfa25-bg: oklch(0.21 0.02 290);
  --color-tfa25-panel: #241f31;
  --color-tfa25-panel: oklch(0.25 0.025 290);
  --color-tfa25-glow: #33244a;
  --color-tfa25-glow: oklch(0.29 0.06 290 / 0.55);
  --color-tfa25-ink: #f2f0f7;
  --color-tfa25-ink: oklch(0.96 0.006 290);
  --color-tfa25-mut: #b8b2c6;
  --color-tfa25-mut: oklch(0.79 0.018 290);
  --color-tfa25-edge: #383246;
  --color-tfa25-edge: oklch(0.34 0.028 290);
  --color-tfa25-accedge: #8b6cc8;
  --color-tfa25-accedge: oklch(0.6 0.12 290);
  --color-tfa25-ring: #4b3a7a;
  --color-tfa25-ring: oklch(0.42 0.1 290 / 0.5);
  --color-tfa25-acc: #c2a8f5;
  --color-tfa25-acc: oklch(0.82 0.13 290);
  --font-tfa25-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa25-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tfa-25 {
  width: 100%;
  min-height: 100svh;
}
/* The listening ring: aria-pressed drives it, but the animation needs a keyframe, and a nested
   aria-pressed:[&>span]: variant would be harder to read than four lines. */

.tfa-25 #tfa25-mic[aria-pressed="true"] > span {
  opacity: 1;
  animation: tfa25-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}

@keyframes tfa25-ping {
  0% {
    transform: scale(1);
    opacity: .55;
  }

  70%,
    100% {
    transform: scale(1.7);
    opacity: 0;
  }
}

.tfa-25 [data-panel] {
  interpolate-size: allow-keywords;
}

@supports (interpolate-size: allow-keywords) {
  .tfa-25 details::details-content {
    height: 0;
    overflow: clip;
    opacity: 0;
    transition: height .35s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .35s allow-discrete;
  }

  .tfa-25 details[open]::details-content {
    height: auto;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tfa-25 #tfa25-mic[aria-pressed="true"] > span {
    animation: none;
    opacity: .5;
  }

  .tfa-25 details::details-content {
    transition-duration: 1ms;
  }
}
(function(){
  var root=document.querySelector('.tfa-25');if(!root||root.dataset.tfa25)return;root.dataset.tfa25='1';
  var input=root.querySelector('#tfa25-input'),status=root.querySelector('#tfa25-status'),
      empty=root.querySelector('#tfa25-empty'),mic=root.querySelector('#tfa25-mic'),
      items=[].slice.call(root.querySelectorAll('#tfa25-list details'));
  items.forEach(function(d){d.dataset.initOpen=d.open?'1':''});
  function filter(){
    var q=input.value.trim().toLowerCase(),hits=0;
    items.forEach(function(d){
      var hit=!q||d.textContent.toLowerCase().indexOf(q)>-1;
      d.hidden=!hit;
      if(q&&hit)d.open=true;
      if(!q)d.open=!!d.dataset.initOpen;
      if(hit)hits++;
    });
    empty.hidden=hits>0;
  }
  input.addEventListener('input',filter);

  var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
  if(!SR){mic.remove();return;}
  var rec=new SR(),listening=false;
  rec.lang=document.documentElement.lang||'en-US';
  rec.interimResults=true;rec.continuous=false;
  function setListening(on){
    listening=on;
    mic.setAttribute('aria-pressed',String(on));
    status.textContent=on?'Listening… speak now':'';
  }
  mic.addEventListener('click',function(){
    if(listening){rec.stop();return;}
    try{rec.start();setListening(true);}catch(e){setListening(false);}
  });
  rec.onresult=function(e){
    var t='';for(var i=0;i<e.results.length;i++)t+=e.results[i][0].transcript;
    input.value=t;filter();
  };
  rec.onerror=function(e){
    setListening(false);
    status.textContent=e.error==='not-allowed'?'Microphone permission denied — the typed filter still works.':'';
  };
  rec.onend=function(){setListening(false);};
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

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

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

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

@theme {
  --color-tfa25-bg: #1c1a26;
  --color-tfa25-bg: oklch(0.21 0.02 290);
  --color-tfa25-panel: #241f31;
  --color-tfa25-panel: oklch(0.25 0.025 290);
  --color-tfa25-glow: #33244a;
  --color-tfa25-glow: oklch(0.29 0.06 290 / 0.55);
  --color-tfa25-ink: #f2f0f7;
  --color-tfa25-ink: oklch(0.96 0.006 290);
  --color-tfa25-mut: #b8b2c6;
  --color-tfa25-mut: oklch(0.79 0.018 290);
  --color-tfa25-edge: #383246;
  --color-tfa25-edge: oklch(0.34 0.028 290);
  --color-tfa25-accedge: #8b6cc8;
  --color-tfa25-accedge: oklch(0.6 0.12 290);
  --color-tfa25-ring: #4b3a7a;
  --color-tfa25-ring: oklch(0.42 0.1 290 / 0.5);
  --color-tfa25-acc: #c2a8f5;
  --color-tfa25-acc: oklch(0.82 0.13 290);
  --font-tfa25-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa25-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tfa-25 {
  width: 100%;
  min-height: 100svh;
}
/* The listening ring: aria-pressed drives it, but the animation needs a keyframe, and a nested
   aria-pressed:[&>span]: variant would be harder to read than four lines. */

.tfa-25 #tfa25-mic[aria-pressed="true"] > span {
  opacity: 1;
  animation: tfa25-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}

@keyframes tfa25-ping {
  0% {
    transform: scale(1);
    opacity: .55;
  }

  70%,
    100% {
    transform: scale(1.7);
    opacity: 0;
  }
}

.tfa-25 [data-panel] {
  interpolate-size: allow-keywords;
}

@supports (interpolate-size: allow-keywords) {
  .tfa-25 details::details-content {
    height: 0;
    overflow: clip;
    opacity: 0;
    transition: height .35s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .35s allow-discrete;
  }

  .tfa-25 details[open]::details-content {
    height: auto;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tfa-25 #tfa25-mic[aria-pressed="true"] > span {
    animation: none;
    opacity: .5;
  }

  .tfa-25 details::details-content {
    transition-duration: 1ms;
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

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

  • aria-pressed: variants are the port's real contribution: the mic's accent fill reads the accessibility attribute, so a toggle button cannot look pressed while announcing itself as unpressed.
  • The ping animation stayed in CSS. Tailwind ships animate-ping, but toggling it from a parent's attribute would mean a nested variant plus an opacity override — four lines of keyframes are more legible, and they give the reduced-motion branch somewhere sane to live.
  • rec.lang = document.documentElement.lang || 'en-US' is a small fix over the original's hardcoded locale — the recogniser now follows the page.
  • UI upgrade from the CSS original: muted copy oklch(0.67) → 0.79, accent 0.78 → 0.82, and the pressed mic now inverts (accent fill, dark glyph) instead of only tinting its border — the listening state was previously carried almost entirely by the animation, which reduced-motion users never saw.
  • UI upgrade: reduced motion gets a static half-opacity ring rather than nothing, so the listening state stays visible without animation.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind FAQ with Voice Search Using the Speech Recognition API
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-faq-voice-search-speech-recognition/

Speak your question and watch the FAQ filter: a microphone button feeds the Speech Recognition API into the same live filter as demo 18, with interim results streaming into the input as you talk and a pulsing listening ring. The part most voice demos skip is the whole point here — the mic only renders where the API exists, the typed filter is the baseline everyone gets, and every state change is announced.
## HTML
```html
<section class="tfa-25 w-full min-h-svh grid place-items-center bg-tfa25-bg font-tfa25-sans text-tfa25-ink px-5 py-12 bg-[radial-gradient(1000px_520px_at_50%_-15%,var(--color-tfa25-glow),transparent_60%)]">
  <div class="w-[min(680px,100%)]">
    <header>
      <h1 class="text-[clamp(28px,5vw,40px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Ask out loud</h1>
      <p class="mt-3 max-w-[56ch] text-[15px] leading-relaxed text-tfa25-mut">Tap the mic and speak, or type — the filter is the same. Voice is the layer on top, never the requirement.</p>
    </header>

    <div class="mt-6 flex min-h-[54px] items-center gap-2.5 rounded-[14px] border border-tfa25-edge bg-tfa25-panel px-4 transition-[border-color,box-shadow] duration-250 focus-within:border-tfa25-acc focus-within:ring-3 focus-within:ring-tfa25-ring">
      <svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" class="shrink-0 text-tfa25-mut"><circle cx="9" cy="9" r="6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M13.5 13.5L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      <input id="tfa25-input" type="search" aria-label="Search or dictate a question" placeholder="Type, or press the mic…" class="min-h-11 w-full min-w-0 flex-1 border-0 bg-transparent text-[15px] text-tfa25-ink outline-none placeholder:text-tfa25-mut [&::-webkit-search-cancel-button]:appearance-none">
      <button type="button" id="tfa25-mic" aria-pressed="false" aria-label="Search by voice" class="relative grid size-11 shrink-0 cursor-pointer place-items-center rounded-full border border-tfa25-edge bg-transparent text-tfa25-acc transition-[background-color,border-color,color] duration-250 hover:border-tfa25-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tfa25-acc aria-pressed:border-tfa25-acc aria-pressed:bg-tfa25-acc aria-pressed:text-tfa25-bg">
        <span aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-full bg-tfa25-acc/40 opacity-0"></span>
        <svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" class="relative"><path d="M12 3.5a3 3 0 013 3v5a3 3 0 01-6 0v-5a3 3 0 013-3z" fill="currentColor"/><path d="M6.5 11a5.5 5.5 0 0011 0M12 16.5V20M9 20h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
      </button>
    </div>
    <p id="tfa25-status" aria-live="polite" class="mt-2 min-h-5 font-tfa25-mono text-[11.5px] text-tfa25-acc"></p>

    <div data-panel id="tfa25-list" class="mt-2 flex flex-col gap-2.5">
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]" open>
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">How do I reset my password?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Use the Forgot password link on the sign-in screen — the reset email arrives within a minute and the link is valid for one hour. Passkeys can be added afterwards from Security settings.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Can I change my plan later?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Any time, in either direction. Upgrades apply instantly and prorate; downgrades take effect at the end of the billing period so you keep what you paid for.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Do you support two-factor authentication?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Yes — authenticator apps and hardware keys on every plan, free, with enforced 2FA available for whole workspaces.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Where can I download invoices?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Billing → History, as PDFs with your tax details. Billing-only members can be added for your accountant.</p>
        </details>
        <details class="group rounded-[13px] border border-tfa25-edge bg-tfa25-panel transition-[border-color,box-shadow] duration-300 open:border-tfa25-accedge open:shadow-[0_12px_30px_-20px_oklch(0.8_0.14_290/0.45)]">
          <summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3.5 text-[15px] font-semibold transition-colors duration-200 hover:text-tfa25-acc focus-visible:rounded-[13px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa25-acc [&::-webkit-details-marker]:hidden">Is there a mobile app?
            <i aria-hidden="true" class="size-2 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa25-acc transition-[rotate,translate] duration-350 group-open:translate-y-0.5 group-open:rotate-[225deg] motion-reduce:transition-none"></i>
          </summary>
          <p class="max-w-[60ch] px-4 pb-3.5 text-[13.5px] leading-[1.7] text-tfa25-mut">Native apps for iOS and Android, plus an installable web app for everything else. Offline edits sync when you reconnect.</p>
        </details>
    </div>

    <div id="tfa25-empty" hidden class="mt-4 rounded-[14px] border border-dashed border-tfa25-edge px-5 py-6 text-center text-tfa25-mut">
      <b class="text-[15px] text-tfa25-ink">Nothing matched that.</b>
      <p class="mt-1.5 text-[13px]">Try fewer words — speech recognition guesses, and sometimes badly.</p>
    </div>

    <p class="mt-5 text-[12.5px] leading-relaxed text-tfa25-mut">Where the API is missing (Firefox today), the mic button is removed at runtime rather than shown broken.</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tfa25-bg: #1c1a26;
  --color-tfa25-bg: oklch(0.21 0.02 290);
  --color-tfa25-panel: #241f31;
  --color-tfa25-panel: oklch(0.25 0.025 290);
  --color-tfa25-glow: #33244a;
  --color-tfa25-glow: oklch(0.29 0.06 290 / 0.55);
  --color-tfa25-ink: #f2f0f7;
  --color-tfa25-ink: oklch(0.96 0.006 290);
  --color-tfa25-mut: #b8b2c6;
  --color-tfa25-mut: oklch(0.79 0.018 290);
  --color-tfa25-edge: #383246;
  --color-tfa25-edge: oklch(0.34 0.028 290);
  --color-tfa25-accedge: #8b6cc8;
  --color-tfa25-accedge: oklch(0.6 0.12 290);
  --color-tfa25-ring: #4b3a7a;
  --color-tfa25-ring: oklch(0.42 0.1 290 / 0.5);
  --color-tfa25-acc: #c2a8f5;
  --color-tfa25-acc: oklch(0.82 0.13 290);
  --font-tfa25-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tfa25-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tfa-25 {
  width: 100%;
  min-height: 100svh;
}
/* The listening ring: aria-pressed drives it, but the animation needs a keyframe, and a nested
   aria-pressed:[&>span]: variant would be harder to read than four lines. */

.tfa-25 #tfa25-mic[aria-pressed="true"] > span {
  opacity: 1;
  animation: tfa25-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}

@keyframes tfa25-ping {
  0% {
    transform: scale(1);
    opacity: .55;
  }

  70%,
    100% {
    transform: scale(1.7);
    opacity: 0;
  }
}

.tfa-25 [data-panel] {
  interpolate-size: allow-keywords;
}

@supports (interpolate-size: allow-keywords) {
  .tfa-25 details::details-content {
    height: 0;
    overflow: clip;
    opacity: 0;
    transition: height .35s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .35s allow-discrete;
  }

  .tfa-25 details[open]::details-content {
    height: auto;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tfa-25 #tfa25-mic[aria-pressed="true"] > span {
    animation: none;
    opacity: .5;
  }

  .tfa-25 details::details-content {
    transition-duration: 1ms;
  }
}
```

## JavaScript
```js
(function(){
  var root=document.querySelector('.tfa-25');if(!root||root.dataset.tfa25)return;root.dataset.tfa25='1';
  var input=root.querySelector('#tfa25-input'),status=root.querySelector('#tfa25-status'),
      empty=root.querySelector('#tfa25-empty'),mic=root.querySelector('#tfa25-mic'),
      items=[].slice.call(root.querySelectorAll('#tfa25-list details'));
  items.forEach(function(d){d.dataset.initOpen=d.open?'1':''});
  function filter(){
    var q=input.value.trim().toLowerCase(),hits=0;
    items.forEach(function(d){
      var hit=!q||d.textContent.toLowerCase().indexOf(q)>-1;
      d.hidden=!hit;
      if(q&&hit)d.open=true;
      if(!q)d.open=!!d.dataset.initOpen;
      if(hit)hits++;
    });
    empty.hidden=hits>0;
  }
  input.addEventListener('input',filter);

  var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
  if(!SR){mic.remove();return;}
  var rec=new SR(),listening=false;
  rec.lang=document.documentElement.lang||'en-US';
  rec.interimResults=true;rec.continuous=false;
  function setListening(on){
    listening=on;
    mic.setAttribute('aria-pressed',String(on));
    status.textContent=on?'Listening… speak now':'';
  }
  mic.addEventListener('click',function(){
    if(listening){rec.stop();return;}
    try{rec.start();setListening(true);}catch(e){setListening(false);}
  });
  rec.onresult=function(e){
    var t='';for(var i=0;i<e.results.length;i++)t+=e.results[i][0].transcript;
    input.value=t;filter();
  };
  rec.onerror=function(e){
    setListening(false);
    status.textContent=e.error==='not-allowed'?'Microphone permission denied — the typed filter still works.':'';
  };
  rec.onend=function(){setListening(false);};
})();
```

How this works

Voice is a layer, never a requirement. The typed filter from demo 18 is the baseline; the mic is added on top and deleted at runtime where the API is missing. That ordering is the entire accessibility and reliability story: no browser sniffing, no broken affordance, and no dependency on a microphone permission for the page to work.

Interim results are what make it feel alive. With interimResults = true the recogniser streams partial transcripts; each one is written into the input and re-runs the filter, so the list narrows while you speak rather than after you stop.

The state lives in one attribute. aria-pressed makes the mic a proper toggle button for AT, and the utilities key off it — the accent fill comes from an aria-pressed: variant, and the pulsing ring from a four-line keyframe scoped to the same attribute. Same principle as demo 21: one source of truth for announced and visual state.

Errors are content, not console noise. A denied microphone permission writes a plain-language line into the live region and returns the user to typing. Voice features fail often and in public; the recovery path is part of the design.

Make it yours

  • Language: set rec.lang from the page's lang attribute rather than hardcoding a locale — this demo does.
  • Continuous dictation: rec.continuous = true keeps listening for multi-sentence queries. Expect more noise in the transcript.
  • Voice actions: match transcripts against commands instead of filtering, and the same wiring becomes a voice nav layer.
  • Privacy note: recognition runs through the browser's speech service. Say so near the mic if your audience cares, and it should.

Gotchas — read before shipping

  • Chrome's implementation sends audio to a remote service. It is not on-device, and a privacy-sensitive product should disclose that.
  • rec.start() throws if called while already running — wrap it, as the script does, or the second click breaks the button.
  • The API is prefixed in Safari and absent in Firefox; feature-detect both names and remove the control otherwise.
  • Never make voice the only input. A microphone prompt on page load is the fastest way to lose a user who just wanted to read.

Browser support

ChromeSafariFirefoxEdge
129+not yetnot yet129+

Floor set by oklch(), interpolate-size as this demo is written. The core technique itself goes back further — Chrome Yes.

Speech Recognition is Chromium and WebKit only; Firefox users get the typed filter with no broken affordance, which is the pattern's own recommendation.

Techniques used in this demo

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…