Speech Synthesis
The Speech Synthesis component provides a comprehensive interface to the Web Speech API's text-to-speech functionality, allowing your Progressive Web App to convert text into spoken words. This enables accessible, hands-free experiences and enhanced user interactions.
This component is particularly useful for:
Accessibility features for users with visual impairments or reading difficulties
Educational applications (language learning, pronunciation guides)
Navigation and turn-by-turn directions
Content reading (news articles, books, messages)
Voice-enabled interfaces and assistive technologies
Multi-language support with native voice selection
Browser Support
The Speech Synthesis API is widely supported across modern browsers:
Chrome/Edge: Full support with extensive voice libraries
Safari: Full support with high-quality voices
Firefox: Full support with system voices
Mobile browsers: Excellent support on both iOS and Android
Usage
Basic Text-to-Speech
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<p>Enter text to be spoken:</p>
<textarea id="speech-text">Hello! Welcome to our Progressive Web App.</textarea>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Hello! Welcome to our Progressive Web App.'
}) }}>
Speak Text
</button>
<div {{ stimulus_target('@pwa/speech-synthesis', 'status') }}></div>
</div>
<script>
document.addEventListener('pwa:speech-synthesis:start', (event) => {
console.log('Speaking:', event.detail.text);
});
document.addEventListener('pwa:speech-synthesis:end', () => {
console.log('Finished speaking');
});
</script>Speaking HTML Elements
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<h2>Article Reader</h2>
<!-- Individual paragraphs with item target -->
<article>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
Click this paragraph to hear it read aloud. The Speech Synthesis API
makes it easy to add text-to-speech to any web application.
</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
You can customize the voice, speed, pitch, and volume for each piece
of content to match your application's needs.
</p>
</article>
<!-- Read all items button -->
<button {{ stimulus_action('@pwa/speech-synthesis', 'enqueueItems', 'click') }}>
Read Entire Article
</button>
</div>Voice Selection with Custom Parameters
<div {{ stimulus_controller('@pwa/speech-synthesis', {
locale: 'en-US',
rate: 1,
pitch: 1,
volume: 1
}) }}>
<h3>Voice Settings</h3>
<!-- Voice selector (automatically populated) -->
<label>Voice:</label>
<select {{ stimulus_target('@pwa/speech-synthesis', 'voiceSelect') }}
{{ stimulus_action('@pwa/speech-synthesis', 'changeVoiceFromSelect', 'change') }}>
</select>
<!-- Speech rate control -->
<label>Speed: <span id="rate-display">1</span>x</label>
<input type="range" min="0.5" max="2" step="0.1" value="1"
{{ stimulus_action('@pwa/speech-synthesis', 'setRate', 'input', {
rate: '@$el.valueAsNumber'
}) }}
oninput="document.getElementById('rate-display').textContent = this.value">
<!-- Pitch control -->
<label>Pitch: <span id="pitch-display">1</span></label>
<input type="range" min="0" max="2" step="0.1" value="1"
{{ stimulus_action('@pwa/speech-synthesis', 'setPitch', 'input', {
pitch: '@$el.valueAsNumber'
}) }}
oninput="document.getElementById('pitch-display').textContent = this.value">
<!-- Volume control -->
<label>Volume: <span id="volume-display">100</span>%</label>
<input type="range" min="0" max="1" step="0.1" value="1"
{{ stimulus_action('@pwa/speech-synthesis', 'setVolume', 'input', {
volume: '@$el.valueAsNumber'
}) }}
oninput="document.getElementById('volume-display').textContent = Math.round(this.value * 100)">
<textarea id="custom-text">This text will be spoken with custom settings.</textarea>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: '@#custom-text.value'
}) }}>
Speak with Custom Settings
</button>
</div>Multi-Language Content
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<h3>Multi-Language Reader</h3>
<div {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="en-US"
data-speech-text="Hello, how are you?"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer; padding: 10px; border: 1px solid #ccc; margin: 5px;">
🇺🇸 English: "Hello, how are you?"
</div>
<div {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="fr-FR"
data-speech-text="Bonjour, comment allez-vous ?"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer; padding: 10px; border: 1px solid #ccc; margin: 5px;">
🇫🇷 French: "Bonjour, comment allez-vous ?"
</div>
<div {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="es-ES"
data-speech-text="Hola, ¿cómo estás?"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer; padding: 10px; border: 1px solid #ccc; margin: 5px;">
🇪🇸 Spanish: "Hola, ¿cómo estás?"
</div>
<div {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="de-DE"
data-speech-text="Hallo, wie geht es dir?"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer; padding: 10px; border: 1px solid #ccc; margin: 5px;">
🇩🇪 German: "Hallo, wie geht es dir?"
</div>
</div>Playback Controls
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: true
}) }}>
<h3>Audio Book Player</h3>
<!-- Content to be read -->
<div id="book-content">
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>
Chapter One: The Beginning. It was a dark and stormy night when our story begins.
</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>
Chapter Two: The Journey. The protagonist set out on an adventure that would change everything.
</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>
Chapter Three: The Discovery. What they found would alter their understanding of the world.
</p>
</div>
<!-- Playback controls -->
<div style="margin-top: 20px;">
<button {{ stimulus_action('@pwa/speech-synthesis', 'enqueueItems', 'click') }}>
▶️ Play All
</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'pause', 'click') }}>
⏸️ Pause
</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'resume', 'click') }}>
⏯️ Resume
</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'cancel', 'click') }}>
⏹️ Stop
</button>
</div>
<div {{ stimulus_target('@pwa/speech-synthesis', 'status') }}
style="margin-top: 10px; font-weight: bold;"></div>
</div>
<script>
document.addEventListener('pwa:speech-synthesis:queued', (event) => {
console.log('Queue size:', event.detail.size);
});
document.addEventListener('pwa:speech-synthesis:dequeue', (event) => {
console.log('Remaining items:', event.detail.remaining);
});
</script>Custom Internationalization
<div {{ stimulus_controller('@pwa/speech-synthesis', {
i18n: {
loading: 'Chargement des voix…',
ready: 'Prêt',
unsupported: 'La synthèse vocale n\'est pas supportée.',
playing: 'Lecture en cours',
paused: 'En pause',
canceled: 'Annulé',
finished: 'Terminé'
}
}) }}>
<h3>Lecteur Français</h3>
<div {{ stimulus_target('@pwa/speech-synthesis', 'status') }}></div>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="fr-FR"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
Cliquez pour écouter ce texte en français.
</p>
</div>Per-Item Custom Parameters
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<h3>Dynamic Speech Parameters</h3>
<!-- Normal speech -->
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="This is normal speed speech."
data-speech-rate="1"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
Normal Speed (1x)
</p>
<!-- Fast speech -->
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="This is fast speech for quick reading."
data-speech-rate="1.5"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
Fast Speed (1.5x)
</p>
<!-- Slow speech -->
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="This is slow speech for careful listening."
data-speech-rate="0.75"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
Slow Speed (0.75x)
</p>
<!-- High pitch -->
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="This text has a higher pitch."
data-speech-pitch="1.5"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
High Pitch
</p>
<!-- Low volume -->
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="This text is quieter."
data-speech-volume="0.5"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}
style="cursor: pointer;">
Low Volume (50%)
</p>
</div>Immediate vs Queue Mode
<!-- Queue mode (default): Utterances are queued and played sequentially -->
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: true
}) }}>
<h3>Queue Mode</h3>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'First message'
}) }}>Speak First</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Second message'
}) }}>Speak Second</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Third message'
}) }}>Speak Third</button>
<p><small>Click multiple buttons - all will be spoken in order</small></p>
</div>
<!-- Immediate mode: New speech cancels previous -->
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: false
}) }}>
<h3>Immediate Mode</h3>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'First message'
}) }}>Speak First</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Second message'
}) }}>Speak Second</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Third message'
}) }}>Speak Third</button>
<p><small>Click multiple buttons - only the latest will be spoken</small></p>
</div>Common Use Cases
1. Accessible News Reader
<div {{ stimulus_controller('@pwa/speech-synthesis', {
rate: 1.2,
enqueue: true
}) }}>
<article>
<h1 {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-rate="0.9">
Breaking News: Important Announcement
</h1>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>
Today's top story covers significant developments in technology...
</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>
Experts believe this will change the industry forever...
</p>
</article>
<button {{ stimulus_action('@pwa/speech-synthesis', 'enqueueItems', 'click') }}>
🔊 Read Article Aloud
</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'cancel', 'click') }}>
Stop Reading
</button>
</div>2. Language Learning App
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<h2>Pronunciation Practice</h2>
<div class="word-card" {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="fr-FR"
data-speech-rate="0.8"
data-speech-text="Bonjour">
<h3>Bonjour</h3>
<p>French greeting - "Hello"</p>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
🔊 Slow (0.8x)
</button>
</div>
<div class="word-card" {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="fr-FR"
data-speech-rate="1.0"
data-speech-text="Bonjour">
<button {{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
🔊 Normal (1x)
</button>
</div>
</div>
<script>
document.addEventListener('pwa:speech-synthesis:boundary', (event) => {
// Highlight word being spoken
console.log('Word boundary at character:', event.detail.charIndex);
});
</script>3. Form Validation Feedback
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: false
}) }}>
<form id="user-form">
<label>Email:</label>
<input type="email" id="email" required>
<button type="submit">Submit</button>
</form>
</div>
<script>
document.getElementById('user-form').addEventListener('submit', (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
if (!email.includes('@')) {
// Trigger speech for error
const event = new CustomEvent('speak-error', {
detail: { text: 'Please enter a valid email address' }
});
// Use Stimulus action programmatically
const controller = document.querySelector('[data-controller="@pwa/speech-synthesis"]');
controller.dispatchEvent(new CustomEvent('click', {
detail: { params: { text: 'Please enter a valid email address' } }
}));
}
});
</script>4. Navigation Assistant
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: false,
rate: 1.1
}) }} data-speech-controller>
<div id="map">
<!-- Map interface -->
</div>
<button onclick="announceDirection('Turn left in 500 meters')">
Simulate Navigation
</button>
</div>
<script>
function announceDirection(instruction) {
const controller = document.querySelector('[data-speech-controller]');
controller.dispatchEvent(new CustomEvent('speak', {
detail: {
params: { text: instruction }
}
}));
}
document.addEventListener('pwa:speech-synthesis:start', () => {
// Could lower background music volume here
console.log('Navigation instruction started');
});
</script>5. Interactive Tutorial System
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: true,
rate: 0.9
}) }}>
<div class="tutorial">
<div class="step" {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="Step one: Click the menu button in the top right corner">
<h3>Step 1</h3>
<p>Click the menu button in the top right corner</p>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
🔊 Listen
</button>
</div>
<div class="step" {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="Step two: Select your preferred language from the dropdown">
<h3>Step 2</h3>
<p>Select your preferred language from the dropdown</p>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
🔊 Listen
</button>
</div>
</div>
<button {{ stimulus_action('@pwa/speech-synthesis', 'enqueueItems', 'click') }}>
🔊 Play Full Tutorial
</button>
</div>API Reference
Values
The controller accepts the following configuration values:
localeValue (String, default: "en-US")
Default language/locale for speech synthesis
Examples:
"en-US","fr-FR","es-ES","de-DE"
rateValue (Number, default: 1)
Speech speed/rate
Range: 0.1 to 10 (typical: 0.5 to 2)
1= normal speed,< 1= slower,> 1= faster
pitchValue (Number, default: 1)
Speech pitch
Range: 0 to 2
1= normal pitch,< 1= lower,> 1= higher
volumeValue (Number, default: 1)
Speech volume
Range: 0 to 1
0= silent,1= maximum volume
voiceValue (String, default: undefined)
Specific voice name to use
If not set, automatically selects best voice for locale
Example:
"Google US English","Microsoft David Desktop"
enqueueValue (Boolean, default: true)
Queue mode behavior
true: Utterances are queued and played sequentiallyfalse: New utterance cancels previous (immediate mode)
i18nValue (Object, default: {})
Internationalization strings for status messages
Keys:
loading,ready,unsupported,playing,paused,canceled,finished
Example:
<div {{ stimulus_controller('@pwa/speech-synthesis', {
locale: 'fr-FR',
rate: 1.2,
pitch: 1,
volume: 0.8,
voice: 'Google français',
enqueue: true,
i18n: {
ready: 'Prêt',
playing: 'Lecture'
}
}) }}>
</div>Targets
item
Marks HTML elements as speakable items
Can be clicked individually or queued together
Supports custom data attributes for per-item configuration
voiceSelect
A
<select>element that will be automatically populated with available voicesAutomatically formatted as:
"Voice Name (locale) • local"or"Voice Name (locale)"
status
Element where status messages are displayed
Shows: "Loading voices…", "Ready", "Playing", "Paused", etc.
Content controlled by
i18nValueor defaults
Example:
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<select {{ stimulus_target('@pwa/speech-synthesis', 'voiceSelect') }}></select>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>Speakable text</p>
<div {{ stimulus_target('@pwa/speech-synthesis', 'status') }}></div>
</div>Actions
speak({ params })
Speaks the provided text
Parameters:
text(String, required): Text to speaklocale(String, optional): Override default localevoice(String, optional): Override default voicerate(Number, optional): Override default ratepitch(Number, optional): Override default pitchvolume(Number, optional): Override default volume
speakItem(event)
Speaks the clicked item target element
Uses
data-speech-*attributes or element'stextContentSupports per-item parameters via data attributes
enqueueItems({ params })
Queues all item targets for sequential playback
Parameters can override defaults for all items
Automatically starts playback
pause()
Pauses current speech
Can be resumed with
resume()
resume()
Resumes paused speech
No effect if not paused
cancel()
Stops current speech and clears queue
Cannot be resumed
changeVoiceFromSelect()
Updates voice from the voiceSelect target value
Automatically bound to voiceSelect change event
setRate({ params })
Updates the speech rate
Parameters:
rate(Number)
setPitch({ params })
Updates the speech pitch
Parameters:
pitch(Number)
setVolume({ params })
Updates the speech volume
Parameters:
volume(Number)
Example:
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click', {
text: 'Hello world',
rate: 1.5
}) }}>Speak Fast</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'pause', 'click') }}>Pause</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'resume', 'click') }}>Resume</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'cancel', 'click') }}>Cancel</button>Item Data Attributes
When using item targets, you can customize speech parameters per element:
data-speech-text
Text to speak (overrides element's
textContent)
data-speech-locale
Language/locale for this item
data-speech-voice
Voice name for this item
data-speech-rate
Speech rate for this item (Number)
data-speech-pitch
Speech pitch for this item (Number)
data-speech-volume
Speech volume for this item (Number)
Example:
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-text="Custom text to speak"
data-speech-locale="fr-FR"
data-speech-rate="1.2"
data-speech-pitch="1.1"
data-speech-volume="0.9"
{{ stimulus_action('@pwa/speech-synthesis', 'speakItem', 'click') }}>
Click to hear custom speech
</p>Events
All events are prefixed with pwa:speech-synthesis: and dispatched on the controller element.
pwa:speech-synthesis:ready
Dispatched when the controller is initialized and ready
No event detail
pwa:speech-synthesis:unsupported
Dispatched if Speech Synthesis API is not supported
No event detail
pwa:speech-synthesis:voicesloaded
Dispatched when voices are loaded and available
Event detail:
voices(Array): List of available voices withname,lang,localproperties
pwa:speech-synthesis:start
Dispatched when speech starts
Event detail:
text(String): The text being spokenlang(String): The language usedsourceEl(Element|undefined): The source element if speaking an item
pwa:speech-synthesis:end
Dispatched when speech finishes
Event detail:
text(String): The text that was spokenlang(String): The language usedsourceEl(Element|undefined): The source element if speaking an item
pwa:speech-synthesis:pause
Dispatched when speech is paused
Event detail:
text(String): The text being pausedsourceEl(Element|undefined): The source element
pwa:speech-synthesis:resume
Dispatched when speech is resumed
Event detail:
text(String): The text being resumedsourceEl(Element|undefined): The source element
pwa:speech-synthesis:error
Dispatched when a speech error occurs
Event detail:
error(String): Error message or typesourceEl(Element|undefined): The source element
pwa:speech-synthesis:boundary
Dispatched at word or sentence boundaries during speech
Event detail:
name(String): Boundary type ("word" or "sentence")charIndex(Number): Character index in the textcharLength(Number): Length of the current word/sentenceelapsedTime(Number): Elapsed time in millisecondssourceEl(Element|undefined): The source element
pwa:speech-synthesis:queued
Dispatched when an utterance is added to the queue
Event detail:
size(Number): Current queue size
pwa:speech-synthesis:dequeue
Dispatched when an utterance is removed from the queue for playback
Event detail:
remaining(Number): Number of items remaining in queue
pwa:speech-synthesis:cancel
Dispatched when speech is canceled
No event detail
pwa:speech-synthesis:voicechange
Dispatched when the voice is changed
Event detail:
name(String): New voice name
pwa:speech-synthesis:ratechange
Dispatched when the rate is changed
Event detail:
rate(Number): New rate value
pwa:speech-synthesis:pitchchange
Dispatched when the pitch is changed
Event detail:
pitch(Number): New pitch value
pwa:speech-synthesis:volumechange
Dispatched when the volume is changed
Event detail:
volume(Number): New volume value
Example:
const controller = document.querySelector('[data-controller="@pwa/speech-synthesis"]');
controller.addEventListener('pwa:speech-synthesis:start', (event) => {
console.log('Started speaking:', event.detail.text);
console.log('Language:', event.detail.lang);
});
controller.addEventListener('pwa:speech-synthesis:boundary', (event) => {
console.log('Boundary:', event.detail.name, 'at', event.detail.charIndex);
// Use for text highlighting, karaoke-style reading, etc.
});
controller.addEventListener('pwa:speech-synthesis:voicesloaded', (event) => {
console.log('Available voices:', event.detail.voices.length);
event.detail.voices.forEach(v => {
console.log(`${v.name} (${v.lang}) ${v.local ? '- Local' : ''}`);
});
});Properties
The controller exposes the following read-only properties:
voices
Returns array of available
SpeechSynthesisVoiceobjectsUpdated when voices are loaded
locales
Returns array of unique locale codes from available voices
Example:
["en-US", "fr-FR", "es-ES"]
isSpeaking
Returns
trueif currently speaking,falseotherwiseBoolean property
Example:
const controller = application.getControllerForElementAndIdentifier(
document.querySelector('[data-controller="@pwa/speech-synthesis"]'),
'@pwa/speech-synthesis'
);
console.log('Available voices:', controller.voices);
console.log('Supported locales:', controller.locales);
console.log('Is speaking:', controller.isSpeaking);Best Practices
1. Always Check Browser Support
document.addEventListener('pwa:speech-synthesis:unsupported', () => {
// Show alternative UI or fallback
document.getElementById('speech-controls').style.display = 'none';
document.getElementById('text-only-mode').style.display = 'block';
});
document.addEventListener('pwa:speech-synthesis:ready', () => {
// Enable speech features
document.getElementById('speech-controls').style.display = 'block';
});2. Provide Visual Feedback
Always show the current state of speech synthesis:
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<div {{ stimulus_target('@pwa/speech-synthesis', 'status') }}
class="speech-status"></div>
</div>
<style>
.speech-status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
</style>
<script>
const statusEl = document.querySelector('.speech-status');
document.addEventListener('pwa:speech-synthesis:start', () => {
statusEl.style.backgroundColor = '#4caf50';
statusEl.style.color = 'white';
});
document.addEventListener('pwa:speech-synthesis:end', () => {
statusEl.style.backgroundColor = '#e0e0e0';
statusEl.style.color = 'black';
});
</script>3. Handle Long Content Appropriately
For long articles, break content into manageable chunks:
<div {{ stimulus_controller('@pwa/speech-synthesis', {
enqueue: true
}) }}>
<!-- Break long content into paragraphs -->
<article>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>First paragraph...</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>Second paragraph...</p>
<p {{ stimulus_target('@pwa/speech-synthesis', 'item') }}>Third paragraph...</p>
</article>
<button {{ stimulus_action('@pwa/speech-synthesis', 'enqueueItems', 'click') }}>
Read Article
</button>
<!-- Allow users to stop long readings -->
<button {{ stimulus_action('@pwa/speech-synthesis', 'cancel', 'click') }}>
Stop Reading
</button>
</div>4. Use Appropriate Speech Rates
Match speech rate to content type:
0.5-0.8: Learning content, complex information, pronunciation guides
0.9-1.0: Standard reading, news articles, general content
1.1-1.5: Quick scanning, familiar content, user preference
1.6-2.0: Rapid reading for advanced users
5. Respect User Preferences
// Save user preferences
document.addEventListener('pwa:speech-synthesis:ratechange', (event) => {
localStorage.setItem('speech-rate', event.detail.rate);
});
document.addEventListener('pwa:speech-synthesis:voicechange', (event) => {
localStorage.setItem('speech-voice', event.detail.name);
});
// Load preferences on page load
const savedRate = localStorage.getItem('speech-rate');
const savedVoice = localStorage.getItem('speech-voice');
if (savedRate || savedVoice) {
// Apply saved preferences to controller values
}6. Optimize for Multi-Language Apps
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<!-- Automatically use correct locale per item -->
<div {{ stimulus_target('@pwa/speech-synthesis', 'item') }}
data-speech-locale="{{ app.request.locale }}"
data-speech-text="{{ 'greeting.text'|trans }}">
{{ 'greeting.text'|trans }}
</div>
</div>7. Use Boundary Events for Highlighting
let currentText = '';
let highlightedEl = null;
document.addEventListener('pwa:speech-synthesis:start', (event) => {
currentText = event.detail.text;
highlightedEl = event.detail.sourceEl;
});
document.addEventListener('pwa:speech-synthesis:boundary', (event) => {
if (event.detail.name === 'word' && highlightedEl) {
// Highlight current word
const start = event.detail.charIndex;
const end = start + event.detail.charLength;
const word = currentText.substring(start, end);
// Update UI to highlight current word
console.log('Current word:', word);
}
});8. Provide Keyboard Controls
Make speech controls accessible via keyboard:
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click') }}
accesskey="s"
title="Press Alt+S to speak">
Speak (Alt+S)
</button>
<button {{ stimulus_action('@pwa/speech-synthesis', 'pause', 'click') }}
accesskey="p"
title="Press Alt+P to pause">
Pause (Alt+P)
</button>
</div>Troubleshooting
No Voices Available
Problem: Voice list is empty or voices don't load
Solutions:
Wait for
voicesloadedevent before using voicesSome browsers load voices asynchronously - the controller handles this automatically
On some systems, text-to-speech voices may need to be installed separately
document.addEventListener('pwa:speech-synthesis:voicesloaded', (event) => {
if (event.detail.voices.length === 0) {
console.warn('No voices available on this system');
// Show message to user about installing TTS voices
}
});Speech Not Working on iOS
Problem: Speech synthesis doesn't work or requires user interaction on iOS Safari
Solutions:
iOS requires user interaction before speech can play - ensure speech is triggered by user action (click, tap)
Don't auto-play speech on page load
Test on actual iOS devices, not just simulators
Voice Selection Not Working
Problem: Selected voice is not being used
Solutions:
Ensure voice name matches exactly (case-sensitive)
Voice must be available in the voices list
Some voices are locale-specific - check voice's
langproperty matches utterance locale
document.addEventListener('pwa:speech-synthesis:voicesloaded', (event) => {
console.log('Available voices:');
event.detail.voices.forEach(v => {
console.log(`Name: "${v.name}", Lang: ${v.lang}`);
});
});Speech Cuts Off or Stops
Problem: Speech stops unexpectedly or gets interrupted
Solutions:
Check for multiple speech synthesis controllers on the same page
Ensure
enqueuemode is set correctlyVerify no JavaScript errors in console
Some browsers limit queue size - break long content into smaller chunks
Rate/Pitch/Volume Not Applied
Problem: Speech parameters don't seem to work
Solutions:
Some voices ignore certain parameters (especially pitch)
Valid ranges: rate (0.1-10), pitch (0-2), volume (0-1)
Not all voices support all parameters - try different voices
Language/Accent Mismatch
Problem: Wrong accent or language is used
Solutions:
Set correct
localevalue (e.g., "en-US" vs "en-GB")Explicitly select a voice with desired accent using
voiceValueUse
data-speech-localeper item for multi-language content
<!-- Explicit locale and voice for best results -->
<div {{ stimulus_controller('@pwa/speech-synthesis', {
locale: 'en-GB',
voice: 'Google UK English Female'
}) }}>Memory Issues with Long Content
Problem: Browser becomes slow or unresponsive with very long content
Solutions:
Break content into smaller chunks (use
itemtargets)Limit queue size - don't enqueue hundreds of items at once
Cancel and clear queue when user navigates away
// Clear queue on page unload
window.addEventListener('beforeunload', () => {
const controller = document.querySelector('[data-controller="@pwa/speech-synthesis"]');
controller?.dispatchEvent(new CustomEvent('cancel'));
});Accessibility Considerations
1. ARIA Attributes
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<button {{ stimulus_action('@pwa/speech-synthesis', 'speak', 'click') }}
aria-label="Read this content aloud"
role="button">
🔊 Listen
</button>
</div>2. Screen Reader Compatibility
The component works alongside screen readers. Provide options to disable speech synthesis if user prefers their own assistive technology:
<div {{ stimulus_controller('@pwa/speech-synthesis') }}>
<label>
<input type="checkbox" id="enable-tts" checked>
Enable text-to-speech
</label>
</div>
<script>
document.getElementById('enable-tts').addEventListener('change', (e) => {
const controller = document.querySelector('[data-controller="@pwa/speech-synthesis"]');
if (!e.target.checked) {
controller.dispatchEvent(new CustomEvent('cancel'));
}
});
</script>3. Visual Indicators
Always provide visual feedback that complements audio:
.speaking {
background-color: #fff9c4;
border-left: 4px solid #fbc02d;
padding-left: 10px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}Last updated
Was this helpful?