How to Configure the Web Speech API for Text-to-Speech Synthesis in a Vue Application
Use the native browser window.speechSynthesis API wrapped in a reactive Vue service, expose it globally via provide/inject, and persist user voice preferences in your store.
The TypeWords project demonstrates a clean pattern for adding speech synthesis to Vue 3 applications. By wrapping the Web Speech API in a reusable helper and integrating it with the existing settings architecture, you can enable text-to-speech functionality without heavy dependencies. This guide walks through the exact implementation used in the zyronon/TypeWords codebase.
Creating a Speech Synthesis Service
The foundation is a thin abstraction around SpeechSynthesisUtterance that handles voice selection, rate/pitch controls, and the asynchronous nature of browser voice loading.
The TTS Helper Module
Create app/utils/tts.js with reactive state and utility functions:
// app/utils/tts.js
import { ref, computed } from 'vue'
const currentVoice = ref(null)
const rate = ref(1)
const pitch = ref(1)
export function getVoices() {
return new Promise(resolve => {
const synth = window.speechSynthesis
const voices = synth.getVoices()
if (voices.length) return resolve(voices)
// Chrome loads voices asynchronously
synth.onvoiceschanged = () => resolve(synth.getVoices())
})
}
export function setVoiceByName(name) {
getVoices().then(voices => {
const v = voices.find(v => v.name === name)
if (v) currentVoice.value = v
})
}
export function speak(text, options = {}) {
if (!window.speechSynthesis) return
const utter = new SpeechSynthesisUtterance(text)
utter.voice = options.voice || currentVoice.value
utter.rate = options.rate ?? rate.value
utter.pitch = options.pitch ?? pitch.value
window.speechSynthesis.speak(utter)
return utter
}
export function cancel() {
window.speechSynthesis.cancel()
}
export const tts = {
currentVoice,
rate,
pitch,
getVoices,
setVoiceByName,
speak,
cancel,
}
Key design decisions in this module:
- Reactive exports –
currentVoice,rate, andpitchare Vue refs, enabling automatic UI updates - Async voice loading – handles both synchronous and asynchronous
getVoices()behavior across browsers - Cancellable utterances – exposes
cancel()for immediate speech interruption
Registering TTS Globally in Vue
The TypeWords application structure uses app/app.vue as the root component. This is the ideal location to expose the service application-wide.
Using Provide/Inject (Composition API)
<script setup>
import { provide } from 'vue'
import { tts } from '@/utils/tts.js'
provide('$tts', tts)
</script>
<template>
<NuxtPage />
</template>
Using Global Properties (Alternative)
For Options API compatibility or migration scenarios:
// Inside app.vue or main entry
export default {
beforeCreate() {
this.appContext.config.globalProperties.$tts = tts
}
}
Either pattern makes $tts accessible throughout the component tree without repeated imports.
Building Voice Selection UI in Settings
The existing settings page at app/pages/setting.vue follows TypeWords' conventions for user-configurable options. Extend it with voice controls:
<template>
<div class="tts-settings">
<label>Voice
<select v-model="selectedVoiceName" @change="onVoiceChange">
<option v-for="v in voices" :key="v.name" :value="v.name">
{{ v.name }}
</option>
</select>
</label>
<label>Rate
<input
type="range"
min="0.5"
max="2"
step="0.1"
v-model.number="rate"
@input="onRateChange"
/>
{{ rate }}×
</label>
<label>Pitch
<input
type="range"
min="0"
max="2"
step="0.1"
v-model.number="pitch"
@input="onPitchChange"
/>
{{ pitch }}
</label>
<button @click="preview">Preview Voice</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getVoices, setVoiceByName, speak, cancel, tts } from '@/utils/tts.js'
import { useStore } from '@/store'
const store = useStore()
const voices = ref([])
const selectedVoiceName = ref(store.ttsVoice || '')
const rate = ref(store.ttsRate ?? 1)
const pitch = ref(store.ttsPitch ?? 1)
onMounted(async () => {
voices.value = await getVoices()
if (!selectedVoiceName.value && voices.value.length) {
selectedVoiceName.value = voices.value[0].name
setVoiceByName(selectedVoiceName.value)
}
})
function onVoiceChange() {
setVoiceByName(selectedVoiceName.value)
store.ttsVoice = selectedVoiceName.value
}
function onRateChange() {
tts.rate.value = store.ttsRate = rate.value
}
function onPitchChange() {
tts.pitch.value = store.ttsPitch = pitch.value
}
function preview() {
cancel()
speak('This is a preview of the selected voice.', {
voice: tts.currentVoice.value,
rate: tts.rate.value,
pitch: tts.pitch.value,
})
}
</script>
The UI pattern mirrors existing components like VolumeSettingMiniDialog.vue, ensuring visual consistency.
Persisting User Preferences
Store configuration in app/store/index.ts using Pinia with persistence:
// app/store/index.ts
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', {
state: () => ({
ttsVoice: '',
ttsRate: 1,
ttsPitch: 1,
}),
persist: true, // Survives page reloads
})
This integration ensures speech settings survive browser sessions without additional localStorage handling.
Triggering Speech in Components
The core typing component WordTypingCore.vue demonstrates practical usage. Inject the service and call speak() on relevant events:
<script setup>
import { inject } from 'vue'
const tts = inject('$tts')
function onCorrectWord(word) {
// Existing scoring logic...
tts.speak(word) // Audible confirmation
}
</script>
For Options API components, access via this.$tts:
methods: {
handleWordSuccess(word) {
this.$tts.speak(word)
}
}
Browser Compatibility and Edge Cases
The Web Speech API implementation varies across browsers:
| Browser | Voice Loading | Default Voice |
|---|---|---|
| Chrome | Asynchronous (onvoiceschanged) |
System default |
| Firefox | Synchronous | First available |
| Safari | Asynchronous | System default |
The getVoices() Promise abstraction in tts.js normalizes these differences. Always check window.speechSynthesis availability before calling methods, as the API is absent in some environments.
Summary
- Create
app/utils/tts.js– WrapSpeechSynthesisUtterancewith reactive Vue refs for voice, rate, and pitch - Register in
app/app.vue– Useprovide('$tts', tts)orglobalPropertiesfor application-wide access - Extend
app/pages/setting.vue– Add voice dropdown, rate/pitch sliders, and preview button - Persist in
app/store/index.ts– Store selections in Pinia withpersist: true - Consume in components – Call
tts.speak(text)from typing handlers likeWordTypingCore.vue
This architecture keeps speech logic decoupled from UI components while maintaining reactive bindings for settings synchronization.
Frequently Asked Questions
How do I handle browsers without Web Speech API support?
Check for window.speechSynthesis before initializing. In tts.js, the speak() function returns early if the API is unavailable. For production applications, consider feature detection and graceful degradation:
const hasTTS = 'speechSynthesis' in window
// Conditionally render TTS controls in UI
Why are voices empty on first load in Chrome?
Chrome loads voices asynchronously. The getVoices() function in tts.js waits for the onvoiceschanged event before resolving. Always use the Promise-based helper rather than calling getVoices() directly.
Can I change speech parameters mid-utterance?
No. The SpeechSynthesisUtterance object captures rate, pitch, and voice at construction time. To change parameters, call cancel() and create a new utterance. The tts.js helper handles this automatically on each speak() call.
How do I queue multiple utterances instead of canceling?
Remove or modify the cancel() call in speak(). By default, the browser queues utterances automatically. The current implementation cancels existing speech for immediate feedback in typing scenarios, but you can adjust this behavior per use case.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →