# How to Add Voice Search to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

Voice search needs three pieces: a microphone button that requests permission, a speech-to-text engine (Web Speech API for zero cost on Chrome/Safari, OpenAI Whisper for Firefox/multilingual accuracy), and a handler that feeds the transcript into your existing search pipeline as if the user had typed it. With Lovable or V0 you can ship a working voice search in 3–5 hours for $0/month — the Web Speech API is free, and Whisper fallback costs pennies at typical usage volumes.

## What Voice Search Actually Is

Voice search is a microphone button layered on top of your existing search feature. Users tap the mic, speak their query, and the app submits it as if they had typed it — the search results are identical. The speech input is handled by the browser's SpeechRecognition API (Web Speech API) for zero cost on Chrome and Safari, or by OpenAI Whisper for broader browser support and higher accuracy in noisy environments or non-English queries. The product decisions are: whether to auto-submit on speech pause or show the transcript first for editing, how to handle mic permission denial gracefully, and whether you need the Whisper fallback for Firefox users. Voice search does not require a new search pipeline — it feeds the existing one.

## Anatomy of the Feature

Six components, none of which require a new backend search system. Voice search routes the transcript into your existing search. The permission handler and the Firefox fallback are where builds actually fail.

- **Mic Button** (ui): An icon button positioned inside the search bar (not beside it). On click, triggers getUserMedia() to request microphone permission on web, or the microphone_access Flutter permission dialog on mobile. Shows a pulsing animated ring while SpeechRecognition is active. Returns to the mic icon when recognition stops.
- **Web Speech API Layer** (service): The SpeechRecognition API (window.webkitSpeechRecognition on Chrome, window.SpeechRecognition standard). Configure with interimResults: true to show live transcript as the user speaks. The onresult event fires with each recognized word; isFinal: true on the last result triggers search submission. The onend event auto-restarts if the user is still speaking.
- **Speech-to-Text Backend (premium/fallback)** (backend): A Supabase Edge Function that receives an audio blob from the client (recorded via MediaRecorder API), calls OpenAI Whisper (/v1/audio/transcriptions) with the audio file, and returns the transcript as JSON. Used for Firefox users, multilingual queries, or high-accuracy requirements where Web Speech API's accuracy is insufficient.
- **Live Transcript Display** (ui): Shows interim results (greyed text) in the search bar input field as the user speaks. When the final result fires, replaces the greyed text with confirmed black text. The cursor is moved to the end of the confirmed transcript. This provides visual feedback that the mic is hearing the user.
- **Search Query Router** (backend): The transcript string is passed directly to the existing search function — Supabase full-text search with to_tsvector, Algolia, or whatever the app already uses. Voice search adds zero new search logic. The transcript must be normalized before submission: trimmed, lowercased, trailing periods and question marks removed.
- **Permission Denial Handler** (ui): When getUserMedia() throws NotAllowedError, displays a tooltip or inline message: 'Microphone access is blocked. To enable: [browser-specific instructions].' Chrome: Settings → Privacy → Microphone. Safari: Preferences → Websites → Microphone. The search bar's text input remains fully functional as the fallback.

## Build paths

### Lovable — fit 4/10, 3–4 hours

Lovable's React/Vite stack supports Web Speech API natively with no backend for English-language search. Add a Whisper Edge Function for Firefox or multilingual support.

1. Create a Lovable project with an existing search feature or a product/content list that can be searched
2. Add your OPENAI_API_KEY to Cloud tab → Secrets if you want the Whisper fallback for Firefox users
3. Paste the prompt below; Agent Mode will add the mic button, Web Speech API wiring, pulsing animation, and permission handler to your search bar
4. Publish to the production URL and test mic permission — the Lovable preview iframe blocks getUserMedia() in most browsers
5. Test Firefox explicitly: confirm the Whisper fallback fires, the MediaRecorder captures audio, and the transcript is returned

Starter prompt:

```
Add voice search to the existing search bar in my app. Requirements: 1) Add a microphone icon button (24px, minimum 44x44px touch target) inside the search input bar on the right side. 2) On click: check if window.SpeechRecognition || window.webkitSpeechRecognition is defined. If yes, create a new SpeechRecognition instance with interimResults: true, continuous: false, lang: 'en-US'. Bind: onresult — for each result, set input value to transcript (grey text for interim, black for final); if isFinal, normalize the transcript (trim, lowercase, remove trailing . ? !) and submit the search form. onend — if user hasn't submitted and recognition stopped, restart if <30 seconds elapsed. onerror — if error is 'not-allowed', show inline tooltip 'Microphone blocked. Allow it in your browser settings.' 3) If SpeechRecognition is not defined (Firefox): use MediaRecorder to record audio from getUserMedia(), stop after silence detection or 10 seconds, POST the audio blob to Supabase Edge Function 'whisper-transcribe' as multipart/form-data with field 'audio', receive transcript JSON, normalize it, and submit the search. 4) Edge Function 'whisper-transcribe': accept multipart audio, call OpenAI /v1/audio/transcriptions with model: whisper-1 and OPENAI_API_KEY from Secrets, return {transcript}. 5) Show a pulsing CSS ring animation on the mic button while recording. Stop animation on onend or after 30 seconds. 6) Keep the text input fully functional — voice is additive, not replacing text entry.
```

Limitations:

- Lovable preview iframe blocks getUserMedia() — always test voice features on the published URL, never in the embedded preview pane
- Web Speech API is unsupported in Firefox; Whisper fallback via Edge Function is required if you need Firefox compatibility
- Web Speech API may prompt the user for mic permission on every new session in some browsers — there is no way to persist permission grant at the application level

### V0 — fit 4/10, 3–4 hours

V0 generates a clean Next.js search bar component with shadcn/ui. Web Speech API works client-side with no backend; a Server Action or API route handles Whisper for Firefox fallback.

1. Paste the prompt below into V0 to generate the voice-search-enhanced search bar component and the Whisper API route
2. Add your OPENAI_API_KEY environment variable in the Vercel Dashboard under Settings → Environment Variables (server-only, no NEXT_PUBLIC_ prefix)
3. Connect to your existing search data source (Supabase, Algolia, or API endpoint) by adding the relevant env vars in Vercel
4. Deploy to production and test mic permission on the live URL — V0's preview sandbox blocks getUserMedia()
5. Verify the Whisper route works by testing in Firefox where Web Speech API is not available

Starter prompt:

```
Add voice search to a Next.js search bar component. The search bar uses shadcn/ui Input and Button components. Requirements: 1) Add a microphone icon button inside the Input using an absolutely positioned div on the right side (padding-right on input to avoid text overlap). 2) The VoiceSearch client component: check window.SpeechRecognition || window.webkitSpeechRecognition on mount. If supported: create SpeechRecognition with interimResults: true; onresult sets input value (grey for interim results, dark for final); when isFinal, normalize transcript (trim, lowercase, remove trailing punctuation), call onSearch(transcript) prop. If not supported (Firefox): record with MediaRecorder using getUserMedia({audio: true}), collect chunks, stop after 10 seconds or silence, POST audio blob to /api/whisper as FormData, receive {transcript}, normalize, call onSearch. 3) API route /api/whisper (POST): parse FormData audio field, call OpenAI /v1/audio/transcriptions with whisper-1 and process.env.OPENAI_API_KEY, return {transcript: string}. 4) Show animated pulse ring on mic button while listening using Tailwind animate-ping. 5) Handle NotAllowedError with inline message 'Microphone blocked — enable in browser settings.' 6) Expose an isListening boolean state so parent components can show a listening indicator in the search results area.
```

Limitations:

- V0 does not auto-provision the Whisper API route's environment variable — OPENAI_API_KEY must be added manually in Vercel Dashboard
- V0's preview sandbox blocks getUserMedia() — test voice search after deploying to Vercel production or a preview deployment
- V0 may choose to implement the pulsing animation with a div instead of Tailwind animate-ping; if the animation doesn't match your design, prompt for a specific Tailwind approach

### Flutterflow — fit 3/10, 4–6 hours

The speech_to_text Flutter package handles microphone natively on iOS and Android. Requires a Custom Code widget and iOS permission configuration. Best when the app is already in FlutterFlow.

1. Add the speech_to_text package under Settings → Pubspec Dependencies in FlutterFlow
2. Create a Custom Widget 'VoiceSearchButton' that imports speech_to_text, initializes SpeechToText(), calls listen() on button tap, and calls a callback function with the recognized words
3. In Settings → Permissions, add NSMicrophoneUsageDescription (iOS): 'Microphone access is used to search by speaking.' — this is required or iOS builds are rejected
4. In Settings → Android Manifest, ensure RECORD_AUDIO permission is included (FlutterFlow typically adds this when you add speech_to_text)
5. Wire the VoiceSearchButton callback to set a Page State variable 'searchQuery', then trigger your existing search action with that value

Limitations:

- speech_to_text requires a Custom Dart code widget in FlutterFlow — it is not a native visual action available in the Action editor
- iOS requires NSMicrophoneUsageDescription in Info.plist or App Store submission is rejected; this must be configured in FlutterFlow Settings → Permissions
- Language support for speech_to_text depends on the OS-level speech recognition languages installed on the device; not all languages are available on all devices

### Custom — fit 5/10, 3–7 days

Full control: Web Speech API for supported browsers + Whisper fallback + custom waveform visualizer + language auto-detection. Worth it when voice search is a core product feature, not an add-on.

1. Implement browser detection: Web Speech API for Chrome and Safari, MediaRecorder + Whisper Edge Function for Firefox; language auto-detect via the browser's navigator.language
2. Build a custom waveform visualizer using AudioContext and AnalyserNode — getByteFrequencyData() at 60fps renders a real-time frequency bar chart next to the search bar during recording
3. Add automatic silence detection: monitor AnalyserNode average amplitude; if below threshold for 1.5 seconds, stop recording and submit
4. Implement query normalization: remove filler words ('um', 'uh', 'search for', 'find me'), normalize Unicode characters, strip trailing punctuation before search submission

Limitations:

- AudioContext waveform visualizer is technically complex and requires careful cleanup of animation frame loops and audio graph connections on component unmount
- Cross-browser mic permission handling has numerous edge cases — iOS Safari requires SpeechRecognition to be created fresh per use; Chrome may throttle long-running recognitions

## Gotchas

- **Microphone permission popup never appears in Lovable or V0 preview** — Both tools run previews inside sandboxed iframes. The browser's getUserMedia() API only works in secure contexts (HTTPS) from a top-level document. Iframes require explicit allow='microphone' attribute on the iframe element to pass permissions through — which Lovable and V0 don't add to their preview iframes. Fix: Publish to the production URL and test voice search there. In Lovable, use the QR code to open the published URL on your phone. In V0, deploy to Vercel before testing any microphone features. Never test mic in the embedded preview.
- **SpeechRecognition is undefined error in Firefox** — Firefox does not implement the Web Speech API as of 2026. If your code checks only window.SpeechRecognition without also checking window.webkitSpeechRecognition, Firefox users see a JavaScript error that breaks the search bar entirely — not a graceful fallback. Fix: At runtime, check const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition. If both are undefined, route to the Whisper Edge Function path using MediaRecorder. Show 'Use Chrome or Safari for instant voice search, or tap the mic to record a query' as a helpful tooltip.
- **Speech recognition stops after 5–10 seconds even with continuous: true** — Some browsers enforce undocumented time limits on SpeechRecognition sessions even when continuous: true is set. Chrome on Android limits sessions to approximately 60 seconds; some Safari versions stop at 20 seconds. The onend event fires, but if you don't handle it, the mic button appears active while recognition has silently stopped. Fix: In the onend handler, check if the user has submitted a query. If not, restart the SpeechRecognition instance with a new object (do not reuse the stopped instance). Add a 30-second hard cap: after 30 seconds without a submission, stop and show 'Listening stopped — tap mic to try again.'
- **Voice search returns transcript but search results are empty** — The browser's speech recognition may return a transcript with trailing punctuation ('show me running shoes.'), filler words, or inconsistent casing ('Running Shoes'). These small differences cause full-text search queries to return zero results even though the content clearly matches — especially with strict text matching in Supabase's to_tsvector or a prefix-based search. Fix: Normalize the transcript before passing it to the search function: transcript.trim().toLowerCase().replace(/[.!?]+$/, ''). Also strip common filler words ('um', 'uh', 'search for', 'find me', 'show me') if they appear at the start of the transcript.
- **iOS Safari speech recognition only works once per page load** — Safari on iOS requires a new SpeechRecognition instance to be created on every use. Reusing the same instance after calling stop() causes recognition to silently fail on subsequent mic button taps — no error is thrown, but no transcript is produced. This makes the mic button appear broken after the first successful search. Fix: Destroy and recreate the SpeechRecognition object inside the mic button click handler every time. Do not cache the instance in state across uses. Set it to null after stop() and create a fresh one on the next tap.

## Best practices

- Always destroy and recreate the SpeechRecognition instance on each mic button tap — especially on iOS Safari where reusing a stopped instance silently fails
- Normalize the transcript before search submission: trim whitespace, lowercase, remove trailing punctuation, strip filler words ('um', 'search for', 'show me')
- Check for both window.SpeechRecognition and window.webkitSpeechRecognition; route to Whisper Edge Function if both are undefined rather than showing an error
- Test on the published HTTPS URL, not in any preview iframe — getUserMedia() is blocked in sandboxed iframes regardless of the tool
- Give the mic button a 44×44px minimum touch target and add aria-label='Search by voice' for screen reader accessibility
- Add a 30-second timeout on active listening — restart on onend if no submission, stop and show a 'tap to retry' message after 30 seconds
- Show the live transcript as the user speaks using interimResults: true — silence while the mic is on makes users think the feature isn't working

## Frequently asked questions

### Does voice search work on all mobile browsers?

Chrome on Android and Safari on iOS both support the Web Speech API. Firefox on Android does not support it as of 2026. For broad mobile coverage, implement the Whisper fallback: detect missing SpeechRecognition support at runtime and switch to MediaRecorder + the Whisper Edge Function path automatically.

### How do I handle microphone permission being denied?

When getUserMedia() throws NotAllowedError, catch it and show a browser-specific help message in a tooltip or inline element: Chrome desktop — three-dot menu → Settings → Privacy and Security → Site Settings → Microphone. Safari — Preferences → Websites → Microphone. On iOS Safari — Settings app → Safari → Microphone. Keep the text input fully functional as the fallback — never disable the search bar when mic is denied.

### Is Web Speech API free?

Yes. The Web Speech API is browser-native and completely free with no rate limits or API keys. The only cost is your server infrastructure for the optional Whisper fallback. At 100–1,000 users, the vast majority will use the free Web Speech API path; Whisper costs only apply to Firefox users and multilingual queries.

### What is the difference between Web Speech API and Whisper?

The Web Speech API uses the browser's built-in speech recognition engine (Google's for Chrome, Apple's for Safari) and works in real time as the user speaks — no audio is sent to your backend. OpenAI Whisper is a cloud API: audio is recorded client-side, sent to your Edge Function, processed by OpenAI's server, and the transcript is returned. Whisper is more accurate for accented speech and non-English queries; Web Speech API has zero latency and zero cost.

### Can voice search work offline?

The Web Speech API requires an internet connection for Chrome (it sends audio to Google's servers). Safari's implementation can use on-device models in some configurations but this is inconsistent across iOS versions. The speech_to_text Flutter package uses the OS speech engine, which on modern iOS and Android can run partially on-device for English. For guaranteed offline voice search, you need an on-device model like Whisper.cpp compiled for mobile — a significant engineering effort.

### How do I show a waveform animation while listening?

For a simple pulsing ring effect, use a CSS animation (Tailwind's animate-ping or a custom keyframe scale animation) triggered by the isListening state. For a real audio waveform visualizer, use the AudioContext API: create an AnalyserNode connected to the microphone stream, call getByteFrequencyData() at 60fps inside a requestAnimationFrame loop, and render the frequency array as bars in a Canvas element. The second approach is significantly more complex but provides genuine audio-reactive feedback.

### Does voice search find the same results as typed search?

Yes — once the transcript is normalized (trim, lowercase, remove trailing punctuation and filler words), it's passed directly to the same search function that handles typed queries. The search engine sees no difference. The only reason voice results diverge from typed results is unnormalized transcripts containing punctuation or filler words that typed queries wouldn't include.

### How do I support multiple languages?

For Web Speech API: set the lang parameter on the SpeechRecognition instance to the user's current locale (navigator.language or your app's active language setting). For Whisper: include the detected language as the language parameter in the /v1/audio/transcriptions call — or omit it for automatic language detection. For the search backend, ensure your full-text search configuration handles the target language's stemming and character sets correctly.

---

Source: https://www.rapidevelopers.com/app-features/voice-search
© RapidDev — https://www.rapidevelopers.com/app-features/voice-search
