JSFiddle - React, Tailwind, and code Playground
by Ben Gillbanks
March 21, 2025
HTML
<article class="blog-post">
<h1>The Joy of Tumbling</h1>
<p>Once upon a time, a block fell. And then another...</p>
</article>
<speech-button selector=".blog-post"></speech-button>
JavaScript
class SpeechButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.button = document.createElement('button');
this.button.textContent = '🔊 Listen';
this.shadowRoot.appendChild(this.button);
this.chunks = [];
this.current = 0;
this.isPlaying = false;
this.isPaused = false;
this.target = null;
}
connectedCallback() {
const selector = this.getAttribute('selector');
this.target = document.querySelector(selector);
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance || !this.target) {
this.button.disabled = true;
this.button.textContent = 'Speech not supported';
return;
}
this.button.addEventListener('click', () => this.toggleSpeech());
window.addEventListener('beforeunload', () => {
speechSynthesis.cancel();
});
}
getChunks(text) {
return text
.match(/[^.!?\n]+[.!?]?|\S+/g)
.map(s => s.trim())
.filter(Boolean);
}
speakNext() {
if (this.current >= this.chunks.length) {
this.isPlaying = false;
this.isPaused = false;
this.button.textContent = '🔊 Listen';
return;
}
const utter = new SpeechSynthesisUtterance(this.chunks[this.current]);
utter.pitch = 1;
utter.rate = 1;
utter.volume = 1;
utter.onend = () => {
this.current++;
if (this.isPlaying && !this.isPaused) {
this.speakNext();
}
};
speechSynthesis.speak(utter);
}
toggleSpeech() {
if (!this.isPlaying && !this.isPaused) {
this.chunks = this.getChunks(this.target.textContent);
this.current = 0;
this.isPlaying = true;
this.isPaused = false;
this.button.textContent = '⏸ Pause';
this.speakNext();
} else if (this.isPlaying && !this.isPaused) {
speechSynthesis.pause();
this.isPaused = true;
this.button.textContent = '▶️ Resume';
} else if (this.isPlaying && this.isPaused) {
speechSynthesis.resume();
this.isPaused = false;
this.button.textContent = '⏸ Pause';
}
}
}
customElements.define('speech-button',...