Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Use Voice Input in Retool Applications

Use the browser's built-in Web Speech API inside a JS Query to capture voice input. Create a SpeechRecognition instance, set onresult to update a Temporary State with the transcript, and call recognition.start() on button click. Bind the Text Input's default value to the state variable. Works in Chrome and Edge; Safari/Firefox have limited support.

What you'll learn

  • Use the Web Speech API's SpeechRecognition interface inside a Retool JS Query
  • Start and stop voice recording from button clicks in your Retool app
  • Populate a Text Input component with the live speech transcription
  • Handle interim (in-progress) vs final transcript results
  • Handle browser compatibility and microphone permission errors gracefully
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner12 min read20-25 minRetool Cloud and Self-hosted (Chrome and Edge only — Web Speech API not supported in Firefox or Safari)Last updated March 2026RapidDev Engineering Team
TL;DR

Use the browser's built-in Web Speech API inside a JS Query to capture voice input. Create a SpeechRecognition instance, set onresult to update a Temporary State with the transcript, and call recognition.start() on button click. Bind the Text Input's default value to the state variable. Works in Chrome and Edge; Safari/Firefox have limited support.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required20-25 min
CompatibilityRetool Cloud and Self-hosted (Chrome and Edge only — Web Speech API not supported in Firefox or Safari)
Last updatedMarch 2026

Adding Voice-to-Text Input to Retool Apps

The Web Speech API is a browser-native API that provides speech recognition without any third-party service. Because Retool apps run in the browser, JS Queries have full access to browser APIs including SpeechRecognition. This means you can add voice input to any Retool form without external API keys or services.

The pattern works as follows: a button triggers a JS Query that creates a SpeechRecognition instance. As the user speaks, the API fires onresult events with live transcripts. The JS Query writes the transcript into a Temporary State variable. A Text Input component reads from that state variable, allowing the user to review and edit before submitting.

Chrome and Edge have full support. Firefox does not implement the Web Speech API. Safari has partial support behind a flag. For cross-browser support, consider a Custom Component wrapping the AssemblyAI or Whisper API instead.

Prerequisites

  • A Retool app with at least one Text Input that should accept voice input
  • Chrome or Edge browser (Web Speech API not available in Firefox/Safari)
  • Microphone access on the device running the Retool app

Step-by-step guide

1

Create a Temporary State to hold the transcript

Voice transcripts are delivered asynchronously via browser events, so you need a Temporary State variable to bridge the async callback and the Retool component. In the left panel, click + State or go to the State tab in the Debug Panel. Create a new Temporary State named 'voiceTranscript' with a default value of an empty string. You will write the recognized speech into this state from the JS Query, and the Text Input component will read from it.

Expected result: Temporary State 'voiceTranscript' created with default value ''.

2

Add a second Temporary State for recording status

You also need to track whether recording is currently active so the UI can show a visual indicator. Create a second Temporary State named 'isRecording' with a default value of false. You will use this to change the button label and color between 'Start Recording' and 'Stop Recording' states, giving the user clear feedback.

Expected result: Temporary State 'isRecording' created with default value false.

3

Create the startVoiceInput JS Query

Create a new JS Query named 'startVoiceInput'. This query initializes the SpeechRecognition API and wires up event handlers to update the Temporary States. The key callbacks are: onstart (set isRecording to true), onresult (update voiceTranscript with the latest transcript), onerror (handle permission denied and other errors), and onend (set isRecording back to false).

typescript
1// JS Query: startVoiceInput
2// Check browser support
3if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
4 utils.showNotification({
5 title: 'Browser not supported',
6 description: 'Voice input requires Chrome or Edge. Your current browser does not support the Web Speech API.',
7 notificationType: 'error',
8 });
9 return;
10}
11
12// Create SpeechRecognition instance
13const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
14const recognition = new SpeechRecognition();
15
16// Configuration
17recognition.lang = 'en-US'; // Language — change to 'es-ES', 'fr-FR', etc. as needed
18recognition.interimResults = true; // Show in-progress text while still speaking
19recognition.maxAlternatives = 1; // Return only the top recognition result
20recognition.continuous = false; // Stop automatically after a pause in speech
21
22// Store recognition on window so stopVoiceInput query can access it
23window._retoolSpeechRecognition = recognition;
24
25recognition.onstart = () => {
26 isRecording.setValue(true);
27 utils.showNotification({
28 title: 'Listening...',
29 description: 'Speak now. Recording will stop automatically after a pause.',
30 notificationType: 'info',
31 duration: 3,
32 });
33};
34
35recognition.onresult = (event) => {
36 let interimTranscript = '';
37 let finalTranscript = '';
38
39 // Iterate over all results
40 for (let i = event.resultIndex; i < event.results.length; i++) {
41 const transcript = event.results[i][0].transcript;
42 if (event.results[i].isFinal) {
43 finalTranscript += transcript;
44 } else {
45 interimTranscript += transcript;
46 }
47 }
48
49 // Update state with current transcript (final preferred, interim as fallback)
50 const currentText = voiceTranscript.value || '';
51 const newText = currentText + (finalTranscript || interimTranscript);
52 voiceTranscript.setValue(newText);
53};
54
55recognition.onerror = (event) => {
56 isRecording.setValue(false);
57
58 const errorMessages = {
59 'not-allowed': 'Microphone access denied. Click the camera/mic icon in your browser address bar to allow microphone access.',
60 'no-speech': 'No speech detected. Make sure your microphone is working and try again.',
61 'network': 'Network error during speech recognition. Check your internet connection.',
62 'aborted': 'Recording was cancelled.',
63 };
64
65 const message = errorMessages[event.error] || `Speech recognition error: ${event.error}`;
66 utils.showNotification({
67 title: 'Voice input error',
68 description: message,
69 notificationType: 'error',
70 });
71};
72
73recognition.onend = () => {
74 isRecording.setValue(false);
75};
76
77// Start recording
78recognition.start();

Expected result: JS Query created. Running it starts the microphone and begins updating voiceTranscript with recognized speech.

4

Create the stopVoiceInput JS Query

Create a second JS Query named 'stopVoiceInput'. This query retrieves the SpeechRecognition instance stored on window in the startVoiceInput query and calls recognition.stop(). The onend callback in startVoiceInput will automatically set isRecording to false when the recognition stops.

typescript
1// JS Query: stopVoiceInput
2// Stops the active SpeechRecognition session
3const recognition = window._retoolSpeechRecognition;
4
5if (recognition) {
6 recognition.stop();
7 window._retoolSpeechRecognition = null;
8} else {
9 utils.showNotification({
10 title: 'Not recording',
11 description: 'No active recording session to stop.',
12 notificationType: 'warning',
13 });
14}

Expected result: stopVoiceInput query stops the active recording session when triggered.

5

Configure the microphone button

Add a Button component to your app. Set the button's label using a conditional expression: {{ isRecording.value ? 'Stop Recording' : 'Start Recording' }}. Set the button color to red when recording: use the button's Style tab to set background color using {{ isRecording.value ? '#ef4444' : '#6366f1' }}. Add an event handler for the onClick event: when isRecording is false, trigger startVoiceInput; when isRecording is true, trigger stopVoiceInput. Use two separate event handlers with 'Only run when' conditions.

typescript
1// Button label expression:
2{{ isRecording.value ? '🎙 Stop Recording' : '🎤 Start Recording' }}
3
4// Event Handler 1: Start recording
5// Trigger: startVoiceInput
6// Only run when: {{ !isRecording.value }}
7
8// Event Handler 2: Stop recording
9// Trigger: stopVoiceInput
10// Only run when: {{ isRecording.value }}

Expected result: Button shows 'Start Recording' by default. Clicking starts recognition. Button turns red and shows 'Stop Recording'. Clicking again stops recording.

6

Bind the Text Input to the voiceTranscript state

Select your Text Input component. In the Inspector, set the Default value to {{ voiceTranscript.value }}. This makes the text input reflect whatever the voice recognition has transcribed. Because it's a Text Input (not read-only), users can also edit the transcribed text before submitting the form — this is important since speech recognition is rarely 100% accurate.

Expected result: Text Input shows the live transcription as the user speaks and remains editable for corrections.

7

Add a language selector for multi-language support

If your app needs to support multiple languages, add a Select component that lets users choose their language before recording. Store the selection in a Temporary State named 'recognitionLang' with default value 'en-US'. In the startVoiceInput query, change the hardcoded 'en-US' to {{ recognitionLang.value }}. Common language codes: 'en-US' (English US), 'en-GB' (English UK), 'es-ES' (Spanish), 'fr-FR' (French), 'de-DE' (German), 'ja-JP' (Japanese), 'zh-CN' (Mandarin).

typescript
1// In startVoiceInput, replace the lang line with:
2recognition.lang = recognitionLang.value || 'en-US';
3
4// Select component options:
5// [
6// { label: 'English (US)', value: 'en-US' },
7// { label: 'English (UK)', value: 'en-GB' },
8// { label: 'Spanish', value: 'es-ES' },
9// { label: 'French', value: 'fr-FR' },
10// { label: 'German', value: 'de-DE' },
11// { label: 'Japanese', value: 'ja-JP' }
12// ]

Expected result: Language selector controls which language the speech recognizer targets.

8

Handle continuous recording mode for long inputs

By default, recognition.continuous = false stops automatically after a pause. For use cases like voice notes or long dictation, set continuous to true. In continuous mode, the recognition does not stop after a pause — you must call recognition.stop() explicitly. Update the startVoiceInput query to use continuous = true for long-form input, and instruct users to click 'Stop Recording' when finished.

typescript
1// For long-form dictation, change:
2recognition.continuous = true; // Don't stop on pause
3recognition.interimResults = true;
4
5// In onresult, accumulate results across multiple speech segments:
6recognition.onresult = (event) => {
7 let fullTranscript = '';
8 for (let i = 0; i < event.results.length; i++) {
9 if (event.results[i].isFinal) {
10 fullTranscript += event.results[i][0].transcript + ' ';
11 }
12 }
13 voiceTranscript.setValue(fullTranscript.trim());
14};

Expected result: In continuous mode, recognition continues across multiple speech segments until the user manually stops it.

Complete working example

JS Query: startVoiceInput (complete implementation)
1// JS Query: startVoiceInput
2// Complete voice input implementation with language selection,
3// interim results, error handling, and continuous mode toggle
4
5// --- Configuration ---
6const USE_CONTINUOUS_MODE = false; // Set to true for long-form dictation
7const LANG = recognitionLang?.value || 'en-US';
8
9// --- Browser support check ---
10if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
11 utils.showNotification({
12 title: 'Browser not supported',
13 description: 'Voice input requires Chrome or Edge. Firefox and Safari do not support the Web Speech API.',
14 notificationType: 'error',
15 });
16 return;
17}
18
19// If already recording, stop instead of starting again
20if (isRecording.value && window._retoolSpeechRecognition) {
21 window._retoolSpeechRecognition.stop();
22 return;
23}
24
25// --- Create recognition instance ---
26const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
27const recognition = new SpeechRecognition();
28
29recognition.lang = LANG;
30recognition.interimResults = true;
31recognition.maxAlternatives = 1;
32recognition.continuous = USE_CONTINUOUS_MODE;
33
34// Store reference for stopVoiceInput query
35window._retoolSpeechRecognition = recognition;
36
37// --- Event handlers ---
38recognition.onstart = () => {
39 isRecording.setValue(true);
40};
41
42recognition.onresult = (event) => {
43 let finalTranscript = '';
44 let interimTranscript = '';
45
46 for (let i = event.resultIndex; i < event.results.length; i++) {
47 const text = event.results[i][0].transcript;
48 if (event.results[i].isFinal) {
49 finalTranscript += text;
50 } else {
51 interimTranscript += text;
52 }
53 }
54
55 if (finalTranscript) {
56 // Append final result to existing transcript
57 const existing = voiceTranscript.value || '';
58 voiceTranscript.setValue((existing + ' ' + finalTranscript).trim());
59 } else if (interimTranscript) {
60 // Show interim results (overwritten when final arrives)
61 // NOTE: For interim-only display, use a separate interimTranscript state
62 // This avoids polluting the final transcript with partial results
63 }
64};
65
66recognition.onerror = (event) => {
67 isRecording.setValue(false);
68 window._retoolSpeechRecognition = null;
69
70 const errorMessages = {
71 'not-allowed': 'Microphone access was denied. Click the lock icon in your browser address bar and allow microphone access, then try again.',
72 'no-speech': 'No speech was detected. Ensure your microphone is working and you are speaking clearly.',
73 'audio-capture': 'No microphone found. Connect a microphone and try again.',
74 'network': 'A network error occurred during recognition. Check your internet connection.',
75 'aborted': 'Recording stopped.',
76 'service-not-allowed': 'Speech recognition service is not allowed. This may occur on HTTP (non-HTTPS) pages.',
77 };
78
79 const message = errorMessages[event.error] || `Recognition error: ${event.error}`;
80 utils.showNotification({
81 title: 'Voice input failed',
82 description: message,
83 notificationType: event.error === 'aborted' ? 'warning' : 'error',
84 });
85};
86
87recognition.onend = () => {
88 isRecording.setValue(false);
89 window._retoolSpeechRecognition = null;
90
91 if (voiceTranscript.value) {
92 utils.showNotification({
93 title: 'Recording complete',
94 description: `Transcribed: "${voiceTranscript.value.slice(0, 60)}${voiceTranscript.value.length > 60 ? '...' : ''}"`,
95 notificationType: 'success',
96 duration: 3,
97 });
98 }
99};
100
101// --- Start recording ---
102try {
103 recognition.start();
104} catch (err) {
105 isRecording.setValue(false);
106 utils.showNotification({
107 title: 'Could not start recording',
108 description: err.message,
109 notificationType: 'error',
110 });
111}

Common mistakes

Why it's a problem: Trying to set a Text Input's value directly from the JS Query using textInput1.setValue() and expecting it to persist

How to avoid: Text Input components in Retool do not have a setValue() method. Instead, use a Temporary State (voiceTranscript.setValue()) and bind the Text Input's Default value to {{ voiceTranscript.value }}. The input will update reactively.

Why it's a problem: Creating a new SpeechRecognition instance every time the user clicks the button without stopping the previous one, causing multiple simultaneous recording sessions

How to avoid: Check if a recognition instance already exists on window._retoolSpeechRecognition before creating a new one. If one is running, call .stop() first. The onend callback cleans up the window reference.

Why it's a problem: Setting interimResults = true and writing every interim result to the final transcript state, causing duplicated words as interim results are replaced by final ones

How to avoid: In the onresult handler, only append to the final transcript state when event.results[i].isFinal is true. Use a separate state or ignore interim results for the value that gets submitted.

Why it's a problem: Not handling the 'not-allowed' error, leaving users confused when microphone permission is denied

How to avoid: In the onerror handler, specifically handle event.error === 'not-allowed' with instructions to click the browser lock icon and enable microphone permissions for the Retool domain.

Best practices

  • Always check for Web Speech API support before calling it — show a clear error message if the user is on an unsupported browser
  • Store the SpeechRecognition instance on window._retoolSpeechRecognition so multiple JS Queries (start and stop) can share the same instance
  • Use Temporary State for the transcript rather than trying to set a Text Input's value directly — Text Input values cannot be set programmatically in Retool without a default value binding
  • Set recognition.continuous = false for short inputs (search fields, single-line forms) and true only for long dictation — continuous mode requires explicit stop and uses more resources
  • Show a visual recording indicator (button color change, animated icon) so users know when the microphone is active — silence during recording is confusing
  • Always allow users to edit the transcribed text before submitting — Web Speech API accuracy is ~95% in ideal conditions but lower in noisy environments or with accents
  • For HTTPS requirement: Web Speech API requires HTTPS in production. Retool Cloud is always HTTPS. Self-hosted Retool must use SSL.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Retool app and want to add voice input to a form using the Web Speech API (no external API keys). I need: (1) a JS Query that initializes SpeechRecognition, starts/stops recording, and writes the transcript to a Retool Temporary State called 'voiceTranscript', (2) how to bind a Text Input's Default value to voiceTranscript.value so it updates as the user speaks, (3) a button that shows 'Start Recording' or 'Stop Recording' based on a 'isRecording' Temporary State, (4) error handling for the 'not-allowed' (microphone denied) case. The app runs in Chrome.

Retool Prompt

Create Retool voice input: 1) Temporary States: voiceTranscript (string, default ''), isRecording (boolean, default false). 2) JS Query 'startVoiceInput': new (window.SpeechRecognition || window.webkitSpeechRecognition)(), set lang='en-US', interimResults=true, onresult updates voiceTranscript.setValue(), onend sets isRecording.setValue(false). 3) Button label {{ isRecording.value ? 'Stop' : 'Start' }}, event handlers trigger startVoiceInput or stopVoiceInput based on isRecording. 4) Text Input Default value={{ voiceTranscript.value }}.

Frequently asked questions

Does the Web Speech API work in Firefox or Safari?

Firefox does not implement the Web Speech API — voice input using this tutorial will not work for Firefox users. Safari has experimental support behind a flag but it's unreliable. For cross-browser voice input, use a Custom Component wrapping a third-party speech-to-text API like OpenAI Whisper or AssemblyAI, which work in all modern browsers via HTTP requests.

Does voice input require any API keys or external services?

No — the Web Speech API is built into Chrome and Edge and uses Google's speech recognition service internally, but this is transparent to you. There are no API keys, no usage limits you need to manage, and no external service to configure. It works as long as the user's browser supports it and they grant microphone permission.

How accurate is the Web Speech API transcription?

The Web Speech API uses Google's speech recognition engine in Chrome. Accuracy is typically 90-95% for clear speech in a quiet environment with a standard US English accent. Accuracy decreases with background noise, non-native accents, technical jargon, or proper nouns. Always allow users to edit the transcription before submitting — provide a Text Input (not a read-only display) so corrections are easy.

Can I use voice input to search a table or trigger a query directly?

Yes — instead of binding the transcript to a Text Input, you can use the onresult callback to directly trigger a Retool query when a final transcript arrives. For example, when finalTranscript is captured, call searchQuery.trigger({ additionalScope: { searchTerm: finalTranscript } }) to immediately execute a search. This creates a hands-free search experience.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.