About this snippet

A voice input box that types what you say. Press the mic, talk, and finished phrases land in the text box while the words still being recognized show in italics underneath. Press the mic again or Esc to stop. The language menu sets the recognition language, and phrases are joined with a space except in Japanese and Chinese, which are written without spaces. You can still type or edit the text by hand at any point.

It uses the SpeechRecognition interface of the Web Speech API, with the webkit prefix as a fallback. continuous keeps it listening between phrases, and interimResults turns on the partial results for the preview. The result handler walks the results from resultIndex and appends only the ones marked isFinal, so partial words never land in the box. The common error codes, from no-speech and audio-capture to not-allowed, each get their own message, and other codes except aborted show a general one.

Speech recognition works in Chrome, Edge and Safari. Firefox does not ship it yet, so the mic button is disabled there with a note. By default, Chrome sends the audio to a server for recognition, so it needs a connection. Serve the page over HTTPS or from localhost, and give an iframe the microphone permission in its allow attribute. Chrome refuses microphone access to a sandboxed frame without allow-same-origin, such as our editor's live preview, so run the code from a local server to try it.

Size
9.0 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
23
<main class="page">
  <section class="dictate" aria-labelledby="dictate-title">
    <div class="dictate-head">
      <h1 class="dictate-title" id="dictate-title">Voice input</h1>
      <label class="lang">
        <span class="sr-only">Language</span>
        <select id="lang">
          <option value="en-US">English (US)</option>
          <option value="en-GB">English (UK)</option>
          <option value="es-ES">Español</option>
          <option value="fr-FR">Français</option>
          <option value="de-DE">Deutsch</option>
          <option value="pt-BR">Português (Brasil)</option>
          <option value="ja-JP">日本語</option>
          <option value="zh-CN">中文</option>
        </select>
      </label>
    </div>
    <label class="sr-only" for="transcript">Message</label>
    <textarea id="transcript" rows="4" placeholder="Press the mic and start talking, or type here"></textarea>
    <p class="interim" id="interim" aria-hidden="true"></p>
    <div class="dictate-bar">
      <p class="status" id="status" role="status">Press the mic to start dictating.</p>
      <button type="button" class="btn-clear" id="clear">Clear</button>
      <button type="button" class="mic" id="mic" aria-pressed="false" aria-label="Voice dictation">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <rect x="9" y="3" width="6" height="11" rx="3"></rect>
          <path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M8.5 21h7"></path>
        </svg>
      </button>
    </div>
  </section>
</main>
*, *::before, *::after {
  box-sizing: border-box;
}

:root {
  --page: #eef2f7;
  --card: #ffffff;
  --text: #0f172a;
  --muted: #5b6678;
  --line: #d5dce6;
  --soft: #eef2f7;
  --accent: #2563eb;
  --live: #dc2626;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: var(--page);
  color: var(--text);
}

.page {
  display: grid;
  grid-template-columns: minmax(0, 560px);
  justify-content: center;
  align-content: center;
  min-height: 100vh;
  padding: 24px 16px;
}

.dictate {
  padding: 20px;
  border-radius: 18px;
  background: var(--card);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 12px 32px -12px rgba(15, 23, 42, 0.2);
}

.dictate-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.dictate-title {
  margin: 0;
  font-size: 1.0625rem;
}

select {
  min-height: 36px;
  padding: 4px 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
}

textarea {
  display: block;
  width: 100%;
  min-height: 112px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  line-height: 1.55;
  resize: vertical;
}

textarea::placeholder {
  color: var(--muted);
}

select:focus-visible,
textarea:focus-visible,
.btn-clear:focus-visible,
.mic:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.interim {
  min-height: 1.5em;
  margin: 8px 2px 0;
  color: var(--muted);
  font-style: italic;
  line-height: 1.5;
}

.dictate-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}

.status {
  flex: 1;
  margin: 0;
  font-size: 0.875rem;
  color: var(--muted);
  line-height: 1.45;
}

.status.is-error {
  color: #b91c1c;
}

.btn-clear {
  min-height: 40px;
  padding: 6px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.btn-clear:hover {
  background: var(--soft);
}

.mic {
  position: relative;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--text);
  color: var(--card);
  cursor: pointer;
  transition: background 0.2s ease;
}

.mic svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.mic[aria-pressed="true"] {
  background: var(--live);
  color: #ffffff;
}

.mic[aria-pressed="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.45);
  animation: rec 1.6s ease-out infinite;
}

.mic:disabled {
  background: var(--line);
  color: var(--muted);
  cursor: not-allowed;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@keyframes rec {
  to {
    box-shadow: 0 0 0 14px rgba(220, 38, 38, 0);
  }
}

@media (pointer: coarse) {
  .btn-clear,
  select {
    min-height: 44px;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #0b1120;
    --card: #151e30;
    --text: #e2e8f0;
    --muted: #a3afc2;
    --line: #334155;
    --soft: #1e293b;
    --accent: #60a5fa;
    --live: #ef4444;
  }

  .status.is-error {
    color: #fca5a5;
  }

  .mic[aria-pressed="true"] {
    color: #0b1120;
  }
}

@media (forced-colors: active) {
  .mic {
    border: 2px solid ButtonText;
  }

  .mic[aria-pressed="true"] {
    forced-color-adjust: none;
    border-color: Highlight;
    background: Highlight;
    color: HighlightText;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mic[aria-pressed="true"]::after {
    animation: none;
  }
}
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;

const errors = {
  "not-allowed": "Microphone access is blocked. Allow it in the site settings. Inside an iframe, the frame also needs microphone in its allow attribute.",
  "service-not-allowed": "The browser's speech service is turned off or not allowed on this page.",
  "audio-capture": "No microphone could be used. Check that one is connected and that this page is allowed to use it.",
  "no-speech": "No speech was heard. Press the mic and try again.",
  "network": "Speech recognition needs a network connection in this browser.",
  "language-not-supported": "That language is not supported here. Pick another one."
};

const textarea = document.getElementById("transcript");
const interim = document.getElementById("interim");
const status = document.getElementById("status");
const mic = document.getElementById("mic");
const clearBtn = document.getElementById("clear");
const lang = document.getElementById("lang");

let recognition = null;
let failed = false;

function setStatus(text, isError) {
  status.textContent = text;
  status.classList.toggle("is-error", Boolean(isError));
}

function setListening(on) {
  mic.setAttribute("aria-pressed", String(on));
  lang.disabled = on;
}

// Add a finished phrase to the end of the text box
function append(text, language) {
  const piece = text.trim();
  if (!piece) return;
  const end = textarea.value.length;
  const noSpace = /^(ja|zh)\b/i.test(language) || !end || /\s$/.test(textarea.value);
  textarea.setRangeText((noSpace ? "" : " ") + piece, end, end, "preserve");
  textarea.scrollTop = textarea.scrollHeight;
}

function start() {
  const rec = new Recognition();
  recognition = rec;
  failed = false;
  rec.lang = lang.value;
  rec.continuous = true;
  rec.interimResults = true;
  setListening(true);
  setStatus("Starting. Allow the microphone if the browser asks.");

  // Events from an older session are ignored
  rec.onstart = function() {
    if (recognition !== rec) return;
    setStatus("Listening. Press the mic again or Esc to stop.");
  };

  // Final results go into the box, the rest is a preview
  rec.onresult = function(e) {
    if (recognition !== rec) return;
    let preview = "";
    for (let i = e.resultIndex; i < e.results.length; i++) {
      const result = e.results[i];
      if (result.isFinal) {
        append(result[0].transcript, rec.lang);
      } else {
        preview += result[0].transcript;
      }
    }
    interim.textContent = preview;
  };

  rec.onerror = function(e) {
    if (recognition !== rec || e.error === "aborted") return;
    failed = true;
    setStatus(errors[e.error] || "Speech recognition stopped with the error " + e.error + ".", true);
  };

  rec.onend = function() {
    if (recognition !== rec) return;
    recognition = null;
    setListening(false);
    interim.textContent = "";
    if (!failed) setStatus("Stopped. Press the mic to keep dictating.");
  };

  try {
    rec.start();
  } catch (err) {
    recognition = null;
    setListening(false);
    setStatus("Speech recognition could not start. " + err.message, true);
  }
}

function stop() {
  if (recognition) recognition.stop();
}

if (!Recognition) {
  mic.disabled = true;
  setStatus("Speech recognition is not available in this browser. It works in Chrome, Edge and Safari.", true);
}

mic.addEventListener("click", function() {
  if (recognition) {
    stop();
  } else {
    start();
  }
});

clearBtn.addEventListener("click", function() {
  textarea.value = "";
  interim.textContent = "";
  textarea.focus();
});

document.addEventListener("keydown", function(e) {
  if (e.key === "Escape" && recognition) stop();
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save