React hooks for the Web Speech API with first-class DX: mic permissions, listening states, browser compatibility, and cursor-aware text insertion.
- ποΈ Mic permission state management β Know if permission is
prompt,granted, ordenied - π Cursor-aware text insertion β Insert transcribed text at cursor position
- π Auto-silence detection β Automatically stop listening after silence
- π Auto-restart on network errors β Resilient to connection issues
- π Browser compatibility β Handles Chrome, Edge, Safari with proper prefixing
- π¦ Tree-shakeable β Only bundle what you use (~5KB gzipped)
- π· TypeScript-first β Full type safety and IDE autocomplete
- βοΈ React 17+ ready β Strict Mode compatible
npm install @syntropy-labs/react-web-speech
# or
yarn add @syntropy-labs/react-web-speech
# or
pnpm add @syntropy-labs/react-web-speechimport { useSpeechInput } from '@syntropy-labs/react-web-speech'
function VoiceInput() {
const {
transcript,
isListening,
isSupported,
permissionState,
start,
stop,
toggle,
} = useSpeechInput({
lang: 'en-US',
continuous: false,
silenceTimeout: 3000,
})
if (!isSupported) {
return <p>Speech recognition is not supported in this browser.</p>
}
return (
<div>
<button onClick={toggle}>
{isListening ? 'π΄ Stop' : 'ποΈ Start'}
</button>
<p>Permission: {permissionState}</p>
<p>Transcript: {transcript}</p>
</div>
)
}The primary hook for speech-to-text functionality.
| Option | Type | Default | Description |
|---|---|---|---|
lang |
string |
navigator.language |
Recognition language (e.g., 'en-US') |
continuous |
boolean |
false |
Keep listening after pause |
interimResults |
boolean |
true |
Show real-time partial results |
maxAlternatives |
number |
1 |
Max alternative transcriptions |
silenceTimeout |
number |
3000 |
Auto-stop after silence (ms), 0 to disable |
autoRestart |
boolean |
false |
Auto-restart on network errors |
onResult |
(text, isFinal) => void |
- | Callback on speech result |
onError |
(error) => void |
- | Callback on error |
onStart |
() => void |
- | Callback when listening starts |
onEnd |
() => void |
- | Callback when listening ends |
| Property | Type | Description |
|---|---|---|
transcript |
string |
Final transcribed text |
interimTranscript |
string |
Real-time partial text |
isListening |
boolean |
Currently listening |
isSupported |
boolean |
Browser supports Speech API |
permissionState |
'prompt' | 'granted' | 'denied' | 'unsupported' |
Mic permission state |
error |
SpeechError | null |
Error details |
start |
() => Promise<void> |
Start listening |
stop |
() => void |
Stop listening gracefully |
abort |
() => void |
Abort listening immediately |
toggle |
() => Promise<void> |
Toggle listening |
clear |
() => void |
Clear transcript and error |
requestPermission |
() => Promise<MicPermissionState> |
Request mic permission |
Extended hook that automatically inserts transcribed text at the cursor position.
import { useSpeechInputWithCursor } from '@syntropy-labs/react-web-speech'
import { useState, useRef } from 'react'
function VoiceTextarea() {
const [value, setValue] = useState('')
const inputRef = useRef<HTMLTextAreaElement>(null)
const { isListening, toggle } = useSpeechInputWithCursor({
inputRef,
value,
onChange: setValue,
appendSpace: true, // Add space after inserted text
})
return (
<div>
<textarea ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} />
<button onClick={toggle}>{isListening ? 'Stop' : 'Speak'}</button>
</div>
)
}| Option | Type | Default | Description |
|---|---|---|---|
inputRef |
RefObject<HTMLInputElement | HTMLTextAreaElement> |
required | Ref to the input element |
value |
string |
required | Current controlled value |
onChange |
(value: string) => void |
required | Value setter |
appendSpace |
boolean |
true |
Add space after inserted text |
| Property | Type | Description |
|---|---|---|
insertAtCursor |
(text: string) => void |
Manually insert text at cursor |
Low-level utilities for cursor position management:
import {
supportsSelection,
getCursorPosition,
setCursorPosition,
insertTextAtCursor
} from '@syntropy-labs/react-web-speech'
// Check if input type supports cursor APIs
supportsSelection(inputElement) // true for text, search, tel, password, url
// Get current cursor position
const { start, end } = getCursorPosition(inputElement)
// Set cursor position (uses requestAnimationFrame for React compatibility)
setCursorPosition(inputElement, position, { focus: true })
// Insert text at cursor in controlled input
insertTextAtCursor(inputRef, 'hello', currentValue, setValue)Note:
numberinput types don't support cursor APIs. The utilities fall back to appending text at the end.
| Browser | Support |
|---|---|
| Chrome / Chromium | β Full |
| Edge | β Full |
| Safari 14.1+ | |
| Firefox | β Not supported |
Note: The Web Speech API requires HTTPS in production (except localhost).
This package is SSR-safe. The Web Speech API is only accessed on the client.
'use client'
import { useSpeechInput } from '@syntropy-labs/react-web-speech'
export function VoiceButton() {
const { isListening, toggle, isSupported } = useSpeechInput()
if (!isSupported) return null
return (
<button onClick={toggle}>
{isListening ? 'Stop' : 'Speak'}
</button>
)
}import dynamic from 'next/dynamic'
const VoiceInput = dynamic(
() => import('../components/VoiceInput'),
{ ssr: false }
)All types are exported:
import type {
UseSpeechInputOptions,
UseSpeechInputReturn,
UseSpeechInputWithCursorOptions,
UseSpeechInputWithCursorReturn,
SpeechError,
SpeechErrorType,
MicPermissionState,
CursorPosition,
BrowserCapabilities,
} from '@syntropy-labs/react-web-speech'import { useState, useRef } from 'react'
import { useSpeechInputWithCursor } from '@syntropy-labs/react-web-speech'
function VoiceForm() {
const [formData, setFormData] = useState({ name: '', email: '' })
const [activeField, setActiveField] = useState<'name' | 'email'>('name')
const inputRefs = {
name: useRef<HTMLInputElement>(null),
email: useRef<HTMLInputElement>(null),
}
const { toggle, isListening } = useSpeechInputWithCursor({
inputRef: inputRefs[activeField],
value: formData[activeField],
onChange: (value) => setFormData({ ...formData, [activeField]: value }),
})
return (
<form>
<input
ref={inputRefs.name}
value={formData.name}
onFocus={() => setActiveField('name')}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
ref={inputRefs.email}
value={formData.email}
onFocus={() => setActiveField('email')}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
<button type="button" onClick={toggle}>
{isListening ? 'π΄ Stop' : 'ποΈ Speak'}
</button>
</form>
)
}import { useSpeechInput } from '@syntropy-labs/react-web-speech'
function LiveTranscript() {
const { transcript, interimTranscript, isListening, toggle } = useSpeechInput({
interimResults: true,
continuous: true,
})
return (
<div>
<button onClick={toggle}>{isListening ? 'Stop' : 'Start'}</button>
<p>
{transcript}
<span style={{ opacity: 0.5 }}>{interimTranscript}</span>
</p>
</div>
)
}The user has denied microphone access. They need to:
- Click the π icon in the browser address bar
- Reset microphone permissions
- Refresh the page
The Web Speech API requires an internet connection. Chrome sends audio to Google's servers for processing.
Some browsers stop recognition after detecting silence. Solutions:
- Use
continuous: truefor longer sessions - Increase
silenceTimeout(or set to0to disable)
The Web Speech API requires HTTPS. Make sure your production site uses SSL.
When testing locally with yarn link, add React aliases to your Vite config:
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
react: path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
},
},
})Existing React speech-to-text packages lack critical production-ready features:
| Feature | Other Packages | This Package |
|---|---|---|
| Mic permission state | β | β |
| Insert text at cursor | β | β |
| Auto-silence detection | β | β |
| Auto-restart on errors | β | β |
| TypeScript-first | Varies | β |
| React 18 Strict Mode | β | β |
Contributions are welcome! Please read our Contributing Guide first.
# Clone the repo
git clone https://github.com/SyntropyLabs/react-web-speech.git
cd react-web-speech
# Install dependencies
yarn install
# Run tests
yarn test
# Type check
yarn typecheck
# Build
yarn buildMIT Β© SyntropyLabs