JSFiddle - React, Tailwind, and code Playground

by Ben Gillbanks

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',...