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

Voice availability varies by platform and language. Desktop browsers typically offer more voices than mobile browsers. The controller automatically handles voice detection and selection.

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 sequentially

  • false: 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 voices

  • Automatically 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 i18nValue or 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 speak

    • locale (String, optional): Override default locale

    • voice (String, optional): Override default voice

    • rate (Number, optional): Override default rate

    • pitch (Number, optional): Override default pitch

    • volume (Number, optional): Override default volume

speakItem(event)

  • Speaks the clicked item target element

  • Uses data-speech-* attributes or element's textContent

  • Supports 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 with name, lang, local properties

pwa:speech-synthesis:start

  • Dispatched when speech starts

  • Event detail:

    • text (String): The text being spoken

    • lang (String): The language used

    • sourceEl (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 spoken

    • lang (String): The language used

    • sourceEl (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 paused

    • sourceEl (Element|undefined): The source element

pwa:speech-synthesis:resume

  • Dispatched when speech is resumed

  • Event detail:

    • text (String): The text being resumed

    • sourceEl (Element|undefined): The source element

pwa:speech-synthesis:error

  • Dispatched when a speech error occurs

  • Event detail:

    • error (String): Error message or type

    • sourceEl (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 text

    • charLength (Number): Length of the current word/sentence

    • elapsedTime (Number): Elapsed time in milliseconds

    • sourceEl (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 SpeechSynthesisVoice objects

  • Updated when voices are loaded

locales

  • Returns array of unique locale codes from available voices

  • Example: ["en-US", "fr-FR", "es-ES"]

isSpeaking

  • Returns true if currently speaking, false otherwise

  • Boolean 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:

  1. Wait for voicesloaded event before using voices

  2. Some browsers load voices asynchronously - the controller handles this automatically

  3. 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:

  1. iOS requires user interaction before speech can play - ensure speech is triggered by user action (click, tap)

  2. Don't auto-play speech on page load

  3. Test on actual iOS devices, not just simulators

Voice Selection Not Working

Problem: Selected voice is not being used

Solutions:

  1. Ensure voice name matches exactly (case-sensitive)

  2. Voice must be available in the voices list

  3. Some voices are locale-specific - check voice's lang property 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:

  1. Check for multiple speech synthesis controllers on the same page

  2. Ensure enqueue mode is set correctly

  3. Verify no JavaScript errors in console

  4. 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:

  1. Some voices ignore certain parameters (especially pitch)

  2. Valid ranges: rate (0.1-10), pitch (0-2), volume (0-1)

  3. Not all voices support all parameters - try different voices

Language/Accent Mismatch

Problem: Wrong accent or language is used

Solutions:

  1. Set correct locale value (e.g., "en-US" vs "en-GB")

  2. Explicitly select a voice with desired accent using voiceValue

  3. Use data-speech-locale per 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:

  1. Break content into smaller chunks (use item targets)

  2. Limit queue size - don't enqueue hundreds of items at once

  3. 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?