25 CSS FAQ Accordions25 / 25

CSS + 40-line JSMIT licensed

FAQ with Voice Search Speech-Recognition API

Speak your question, watch the FAQ filter: a microphone button feeds the Web Speech API's SpeechRecognition into the same live filter as demo 18, with interim results streaming into the input as you talk, a pulsing listening indicator, and — the part most voice demos skip — a complete progressive-enhancement story: the mic button only renders where the API exists, the keyboard filter is the baseline everyone gets, and every state change is announced for screen readers. Voice as a bonus layer, never a requirement.

Published

Live Demo
Try it

The code

<section class="cfa-25" aria-label="FAQ with voice search">
  <div class="cfa-25__wrap">
    <header class="cfa-25__head">
      <h1>Ask out loud</h1>
      <p>Tap the mic and say “warranty” or “battery” — or just type. Voice is a layer on the same filter, not a requirement.</p>
    </header>
    <div class="cfa-25__bar">
      <input type="search" id="cfa25-input" class="cfa-25__input" placeholder="Type or speak to filter…" aria-label="Search frequently asked questions by typing or voice">
      <button type="button" class="cfa-25__mic" id="cfa25-mic" aria-pressed="false" aria-label="Search by voice">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 3a3 3 0 013 3v5a3 3 0 01-6 0V6a3 3 0 013-3z" fill="currentColor"/><path d="M6 11a6 6 0 0012 0M12 17v3.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      </button>
    </div>
    <p class="cfa-25__status" id="cfa25-status" aria-live="polite"></p>
    <div class="cfa-25__list" id="cfa25-list">
      <details class="cfa-25__item">
        <summary class="cfa-25__q">How long does the battery last?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Up to 14 days in watch mode, 30 hours with continuous GPS. Battery care mode (on by default) caps daily charging to double the cell's lifespan.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">What does the warranty cover?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Two years on manufacturing defects and battery health below 80%. Accidental damage falls under the optional care plan with flat-rate repairs.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">Can I swim with it?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Yes — 5ATM rated for pool and open water, with a dedicated swim-tracking mode. Rinse with fresh water after the sea; salt is the enemy of every seal.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">Does voice control work offline?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Basic commands (timers, workouts, music controls) run on-device. Free-form questions need a connection — the same trade this very page makes with browser speech recognition.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">How do I update the firmware?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Updates install automatically overnight while charging, when on Wi-Fi. You can trigger one manually from the app under Device → Software.</p></div>
      </details>
    </div>
    <div class="cfa-25__empty" id="cfa25-empty" hidden><b>No matches.</b><p>Try different words, or clear the search.</p></div>
    <p class="cfa-25__foot">Voice audio is processed by your browser vendor's speech service. No mic in this browser? The typed filter is the baseline. CodeFronts collection.</p>
  </div>
</section>
.cfa-25,
.cfa-25 *,
.cfa-25 *::before,
.cfa-25 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cfa-25 {
  --bg: oklch(0.15 0.025 310);
  --panel: oklch(0.2 0.028 310);
  --ink: oklch(0.96 0.006 305);
  --mut: oklch(0.68 0.02 308);
  --line: oklch(0.31 0.03 309);
  --acc: oklch(0.75 0.17 350);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(20px,4vw,56px);
  background: radial-gradient(950px 550px at 20% -10%,oklch(0.24 0.06 350/.5),transparent 60%),var(--bg);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cfa-25__wrap {
  width: min(100%,640px);
}

.cfa-25__head h1 {
  font-size: clamp(28px,4.5cqi,40px);
  font-weight: 800;
  letter-spacing: -.025em;
}

.cfa-25__head p {
  margin-top: 8px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--mut);
}

.cfa-25__bar {
  display: flex;
  align-items: stretch;
  gap: 10px;
  margin-top: 22px;
}

.cfa-25__input {
  flex: 1;
  min-width: 0;
  min-height: 52px;
  padding: 0 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  outline: 0;
  transition: border-color .25s,box-shadow .25s;
}

.cfa-25__input::placeholder {
  color: var(--mut);
}

.cfa-25__input:focus-visible {
  border-color: var(--acc);
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--acc) 25%,transparent);
}

.cfa-25__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

.cfa-25__mic {
  position: relative;
  flex: none;
  width: 52px;
  min-height: 52px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--panel);
  color: var(--mut);
  cursor: pointer;
  transition: color .25s,border-color .25s,background .25s;
}

.cfa-25__mic:hover {
  color: var(--acc);
  border-color: color-mix(in oklch,var(--acc) 45%,var(--line));
}

.cfa-25__mic:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cfa-25__mic[aria-pressed="true"] {
  color: oklch(0.98 0.005 350);
  background: var(--acc);
  border-color: var(--acc);
}

.cfa-25__mic[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 18px;
  border: 2px solid var(--acc);
  animation: cfa25pulse 1.3s ease-out infinite;
}

@keyframes cfa25pulse {
  0% {
    opacity: .8;
    scale: 1;
  }

  100% {
    opacity: 0;
    scale: 1.25;
  }
}

.cfa-25__status {
  min-height: 20px;
  margin-top: 10px;
  font-family: ui-monospace,Menlo,monospace;
  font-size: 12px;
  color: var(--acc);
}

.cfa-25__list {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  interpolate-size: allow-keywords;
}

.cfa-25__item {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 13px;
  transition: border-color .3s;
}

.cfa-25__item[open] {
  border-color: color-mix(in oklch,var(--acc) 40%,var(--line));
}

.cfa-25__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 44px;
  padding: 15px 17px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 650;
  list-style: none;
  transition: color .2s;
}

.cfa-25__q::-webkit-details-marker {
  display: none;
}

.cfa-25__q:hover {
  color: var(--acc);
}

.cfa-25__q:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -2px;
  border-radius: 13px;
}

.cfa-25__q i {
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--acc);
  border-bottom: 2px solid var(--acc);
  rotate: 45deg;
  translate: 0 -2px;
  transition: rotate .35s;
}

.cfa-25__item[open] .cfa-25__q i {
  rotate: 225deg;
  translate: 0 2px;
}

.cfa-25__a p {
  padding: 0 17px 16px;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 60ch;
}

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

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

.cfa-25__empty {
  margin-top: 14px;
  padding: 24px 20px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: 14px;
  color: var(--mut);
}

.cfa-25__empty b {
  color: var(--ink);
}

.cfa-25__empty p {
  margin-top: 5px;
  font-size: 13px;
}

.cfa-25__foot {
  margin-top: 20px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
}

@media (prefers-reduced-motion: reduce) {
  .cfa-25 *,
    .cfa-25 .cfa-25__item::details-content {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
  }
}
(function(){
  var root=document.querySelector('.cfa-25');if(!root||root.dataset.cfa25)return;root.dataset.cfa25='1';
  var input=root.querySelector('#cfa25-input'),status=root.querySelector('#cfa25-status'),
      empty=root.querySelector('#cfa25-empty'),mic=root.querySelector('#cfa25-mic'),
      items=[].slice.call(root.querySelectorAll('.cfa-25__item'));
  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=false;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='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);};
})();
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 FAQ Accordion 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: FAQ with Voice Search Speech-Recognition API
Source: https://codefronts.com/snippets/css-faq-accordion/faq-with-voice-search-speech-recognition-api/

Speak your question, watch the FAQ filter: a microphone button feeds the Web Speech API's SpeechRecognition into the same live filter as demo 18, with interim results streaming into the input as you talk, a pulsing listening indicator, and — the part most voice demos skip — a complete progressive-enhancement story: the mic button only renders where the API exists, the keyboard filter is the baseline everyone gets, and every state change is announced for screen readers. Voice as a bonus layer, never a requirement.
## HTML
```html
<section class="cfa-25" aria-label="FAQ with voice search">
  <div class="cfa-25__wrap">
    <header class="cfa-25__head">
      <h1>Ask out loud</h1>
      <p>Tap the mic and say “warranty” or “battery” — or just type. Voice is a layer on the same filter, not a requirement.</p>
    </header>
    <div class="cfa-25__bar">
      <input type="search" id="cfa25-input" class="cfa-25__input" placeholder="Type or speak to filter…" aria-label="Search frequently asked questions by typing or voice">
      <button type="button" class="cfa-25__mic" id="cfa25-mic" aria-pressed="false" aria-label="Search by voice">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 3a3 3 0 013 3v5a3 3 0 01-6 0V6a3 3 0 013-3z" fill="currentColor"/><path d="M6 11a6 6 0 0012 0M12 17v3.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      </button>
    </div>
    <p class="cfa-25__status" id="cfa25-status" aria-live="polite"></p>
    <div class="cfa-25__list" id="cfa25-list">
      <details class="cfa-25__item">
        <summary class="cfa-25__q">How long does the battery last?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Up to 14 days in watch mode, 30 hours with continuous GPS. Battery care mode (on by default) caps daily charging to double the cell's lifespan.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">What does the warranty cover?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Two years on manufacturing defects and battery health below 80%. Accidental damage falls under the optional care plan with flat-rate repairs.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">Can I swim with it?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Yes — 5ATM rated for pool and open water, with a dedicated swim-tracking mode. Rinse with fresh water after the sea; salt is the enemy of every seal.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">Does voice control work offline?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Basic commands (timers, workouts, music controls) run on-device. Free-form questions need a connection — the same trade this very page makes with browser speech recognition.</p></div>
      </details>
      <details class="cfa-25__item">
        <summary class="cfa-25__q">How do I update the firmware?<i aria-hidden="true"></i></summary>
        <div class="cfa-25__a"><p>Updates install automatically overnight while charging, when on Wi-Fi. You can trigger one manually from the app under Device → Software.</p></div>
      </details>
    </div>
    <div class="cfa-25__empty" id="cfa25-empty" hidden><b>No matches.</b><p>Try different words, or clear the search.</p></div>
    <p class="cfa-25__foot">Voice audio is processed by your browser vendor's speech service. No mic in this browser? The typed filter is the baseline. CodeFronts collection.</p>
  </div>
</section>
```
## CSS
```css
.cfa-25,
.cfa-25 *,
.cfa-25 *::before,
.cfa-25 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cfa-25 {
  --bg: oklch(0.15 0.025 310);
  --panel: oklch(0.2 0.028 310);
  --ink: oklch(0.96 0.006 305);
  --mut: oklch(0.68 0.02 308);
  --line: oklch(0.31 0.03 309);
  --acc: oklch(0.75 0.17 350);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(20px,4vw,56px);
  background: radial-gradient(950px 550px at 20% -10%,oklch(0.24 0.06 350/.5),transparent 60%),var(--bg);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cfa-25__wrap {
  width: min(100%,640px);
}

.cfa-25__head h1 {
  font-size: clamp(28px,4.5cqi,40px);
  font-weight: 800;
  letter-spacing: -.025em;
}

.cfa-25__head p {
  margin-top: 8px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--mut);
}

.cfa-25__bar {
  display: flex;
  align-items: stretch;
  gap: 10px;
  margin-top: 22px;
}

.cfa-25__input {
  flex: 1;
  min-width: 0;
  min-height: 52px;
  padding: 0 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  outline: 0;
  transition: border-color .25s,box-shadow .25s;
}

.cfa-25__input::placeholder {
  color: var(--mut);
}

.cfa-25__input:focus-visible {
  border-color: var(--acc);
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--acc) 25%,transparent);
}

.cfa-25__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

.cfa-25__mic {
  position: relative;
  flex: none;
  width: 52px;
  min-height: 52px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--panel);
  color: var(--mut);
  cursor: pointer;
  transition: color .25s,border-color .25s,background .25s;
}

.cfa-25__mic:hover {
  color: var(--acc);
  border-color: color-mix(in oklch,var(--acc) 45%,var(--line));
}

.cfa-25__mic:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cfa-25__mic[aria-pressed="true"] {
  color: oklch(0.98 0.005 350);
  background: var(--acc);
  border-color: var(--acc);
}

.cfa-25__mic[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 18px;
  border: 2px solid var(--acc);
  animation: cfa25pulse 1.3s ease-out infinite;
}

@keyframes cfa25pulse {
  0% {
    opacity: .8;
    scale: 1;
  }

  100% {
    opacity: 0;
    scale: 1.25;
  }
}

.cfa-25__status {
  min-height: 20px;
  margin-top: 10px;
  font-family: ui-monospace,Menlo,monospace;
  font-size: 12px;
  color: var(--acc);
}

.cfa-25__list {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  interpolate-size: allow-keywords;
}

.cfa-25__item {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 13px;
  transition: border-color .3s;
}

.cfa-25__item[open] {
  border-color: color-mix(in oklch,var(--acc) 40%,var(--line));
}

.cfa-25__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 44px;
  padding: 15px 17px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 650;
  list-style: none;
  transition: color .2s;
}

.cfa-25__q::-webkit-details-marker {
  display: none;
}

.cfa-25__q:hover {
  color: var(--acc);
}

.cfa-25__q:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -2px;
  border-radius: 13px;
}

.cfa-25__q i {
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--acc);
  border-bottom: 2px solid var(--acc);
  rotate: 45deg;
  translate: 0 -2px;
  transition: rotate .35s;
}

.cfa-25__item[open] .cfa-25__q i {
  rotate: 225deg;
  translate: 0 2px;
}

.cfa-25__a p {
  padding: 0 17px 16px;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 60ch;
}

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

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

.cfa-25__empty {
  margin-top: 14px;
  padding: 24px 20px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: 14px;
  color: var(--mut);
}

.cfa-25__empty b {
  color: var(--ink);
}

.cfa-25__empty p {
  margin-top: 5px;
  font-size: 13px;
}

.cfa-25__foot {
  margin-top: 20px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
}

@media (prefers-reduced-motion: reduce) {
  .cfa-25 *,
    .cfa-25 .cfa-25__item::details-content {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
  }
}
```

## JavaScript
```js
(function(){
  var root=document.querySelector('.cfa-25');if(!root||root.dataset.cfa25)return;root.dataset.cfa25='1';
  var input=root.querySelector('#cfa25-input'),status=root.querySelector('#cfa25-status'),
      empty=root.querySelector('#cfa25-empty'),mic=root.querySelector('#cfa25-mic'),
      items=[].slice.call(root.querySelectorAll('.cfa-25__item'));
  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=false;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='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

SpeechRecognition (still webkit-prefixed in Chromium, its home turf) streams transcription events; wiring it to an existing filter is ~40 honest lines:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) micBtn.remove();               // enhancement, not requirement

const rec = new SR();
rec.lang = 'en-US';
rec.interimResults = true;              // words appear AS you speak

rec.onresult = (e) => {
  input.value = [...e.results].map(r => r[0].transcript).join('');
  filter();                             // same function typing uses
};
rec.onend = () => setListening(false);  // fires on silence or stop

interimResults is the UX difference between magic and dead air — provisional words render in the input within ~200ms and firm up as the recognizer commits. Because the transcript lands in the same input and calls the same filter() as typing, voice inherits everything demo 18 built: match highlighting scope, the count's aria-live announcements, the empty state. Voice is an input method here, not a separate feature — the architectural point of the demo.

The listening state does triple duty: the button gets aria-pressed + a CSS pulse ring (@keyframes on a pseudo-element, GPU-cheap), a status line announces "Listening…" via the existing live region, and onend — which fires when the user stops talking, denies the mic, or the tab loses focus — cleans all of it up through one path. Privacy note baked into the UI copy: recognition audio is processed by the browser vendor's service in most engines; the demo says so under the mic instead of hiding it.

Make it yours

  • Language: rec.lang drives recognition — wire it to your page's lang attribute, or offer a picker for multilingual help centers.
  • Continuous mode: rec.continuous = true keeps listening across pauses — right for dictation, wrong for one-shot search (this demo keeps it false).
  • Wake behavior: auto-submit when confidence > .85 on a final result, or leave the transcript for the user to confirm — this demo leaves it, respecting the correct-then-search habit.
  • Fallback UX: where the API is missing, the demo removes the mic silently; a title-attribute explainer or a 'voice not supported here' tooltip suits products where users expect it.

Gotchas — read before shipping

  • Chrome ships it as webkitSpeechRecognition and requires HTTPS (or localhost) + mic permission — test the denial path; onerror('not-allowed') must reset the UI.
  • Firefox has no SpeechRecognition at all (2026) — feature-detect and remove the button; never show a mic that throws.
  • Do NOT restart recognition in onend unconditionally — a permission denial also fires onend, and auto-restart creates an infinite permission-prompt loop.
  • iOS Safari supports the API but routes through Siri's engine with stricter timeouts — keep sessions one-shot (continuous=false) for reliability there.
  • Sandboxed iframes without allow="microphone" silently block mic access — the typed search filter above still works, but voice needs the iframe permission (or paste the code onto your own page where it runs unrestricted).

Browser support

ChromeSafariFirefoxEdge
129+not yetnot yet129+

Floor set by oklch(), color-mix(), interpolate-size as this demo is written. The core technique itself goes back further — Chrome 33+ (webkit prefix).

SpeechRecognition: Chromium and Safari yes, Firefox no — which is exactly why the demo is built as progressive enhancement over the universal typed filter. The page never breaks; it just loses the mic.

Techniques used in this demo

Search CodeFronts

Loading…