Text-to-Speech Converter with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #18
Project Overview
A small, beginner-friendly Text-to-Speech (TTS) Converter using the browser Web Speech API (speechSynthesis). Users can paste or type text, choose a voice, adjust rate & pitch, and play/pause/stop the spoken output. This demonstrates the Speech Synthesis API, enumerating voices, simple state management, and accessible controls.
Key Features
- Enter text (multi-line) to speak.
- Select available system/browser voices.
- Control rate, pitch, and volume.
- Play / Pause / Resume / Stop controls.
- Visual speaking state and basic error handling.
- Accessible labels, keyboard-friendly controls, and
aria-livestatus messages. - Graceful note if
speechSynthesisis unavailable.
HTML Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 18 — Text-to-Speech Converter</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="card" role="main" aria-labelledby="title">
<header>
<div class="logo">TTS</div>
<div>
<h1 id="title">Day 18: Text-to-Speech Converter</h1>
<p class="lead">Convert text to speech using the Web Speech API. Choose a voice, adjust rate/pitch, then Play.</p>
</div>
</header>
<section class="panel">
<label class="field">
<span class="label">Text to speak</span>
<textarea id="text" rows="6" placeholder="Type or paste text here...">Hello — this is a text to speech test.</textarea>
</label>
<div class="row controls">
<label class="small">
Voice
<select id="voiceSelect" aria-label="Voice selection"></select>
</label>
<label class="small">
Rate <span id="rateVal">1</span>
<input id="rate" type="range" min="0.5" max="2" step="0.1" value="1" />
</label>
<label class="small">
Pitch <span id="pitchVal">1</span>
<input id="pitch" type="range" min="0" max="2" step="0.1" value="1" />
</label>
<label class="small">
Volume <span id="volVal">1</span>
<input id="volume" type="range" min="0" max="1" step="0.05" value="1" />
</label>
</div>
<div class="actions">
<button id="playBtn" class="btn">Play</button>
<button id="pauseBtn" class="btn secondary" disabled>Pause</button>
<button id="resumeBtn" class="btn secondary" disabled>Resume</button>
<button id="stopBtn" class="btn secondary" disabled>Stop</button>
</div>
<div id="status" class="muted" role="status" aria-live="polite">Ready.</div>
<details style="margin-top:12px">
<summary>How it works (short)</summary>
<p class="small">The script populates available voices (async), creates a <code>SpeechSynthesisUtterance</code> for the text, sets voice, rate, pitch and volume, and uses <code>speechSynthesis.speak()</code>. Pause/resume/stop use the API methods. Some browsers require a user gesture to start audio.</p>
</details>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
CSS Code
:root {
--bg: #071026;
--card: #0b1220;
--accent: #7c3aed;
--muted: #9aa4b2;
--white: #e6eef6;
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}
html,
body {
height: 100%;
}
body {
margin: 0;
background: #002252;
color: var(--white);
display: flex;
align-items: center;
justify-content: center;
padding: 28px;
}
.card {
width: min(920px, 96%);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border-radius: 12px;
padding: 18px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.03);
}
header {
display: flex;
gap: 12px;
align-items: center;
}
.logo {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), #22c1c3);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
h1 {
margin: 0;
font-size: 18px;
}
.lead {
margin: 4px 0 12px;
color: var(--muted);
font-size: 14px
}
.panel {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
}
.label {
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
textarea {
min-height: 96px;
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.04);
background: transparent;
color: inherit;
outline: none;
resize: vertical;
}
.row.controls {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.small {
font-size: 13px;
color: var(--muted);
display: flex;
flex-direction: column;
gap: 6px;
min-width: 160px;
}
input[type=range] {
width: 160px
}
.actions {
display: flex;
gap: 8px;
align-items: center
}
.btn {
padding: 10px 14px;
border-radius: 10px;
border: 0;
background: linear-gradient(90deg, var(--accent), #22c1c3);
color: white;
font-weight: 600;
cursor: pointer
}
.btn.secondary {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.06);
color: var(--muted)
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed
}
.muted {
color: var(--muted);
font-size: 13px
}
.small-note {
font-size: 12px;
color: var(--muted)
} Javascript Code
// Day 18 — Text-to-Speech Converter using Web Speech API
const textEl = document.getElementById('text');
const voiceSelect = document.getElementById('voiceSelect');
const rateEl = document.getElementById('rate');
const pitchEl = document.getElementById('pitch');
const volumeEl = document.getElementById('volume');
const rateVal = document.getElementById('rateVal');
const pitchVal = document.getElementById('pitchVal');
const volVal = document.getElementById('volVal');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const stopBtn = document.getElementById('stopBtn');
const statusEl = document.getElementById('status');
let synth = window.speechSynthesis || null;
let voices = [];
let utter = null; // current SpeechSynthesisUtterance
// Utils
function setStatus(msg, isError=false) {
statusEl.textContent = msg;
statusEl.style.color = isError ? '#ffbaba' : '';
}
// Check support
if (!synth) {
// No Web Speech API support
setStatus('Speech Synthesis API not supported in this browser.', true);
// disable controls
[playBtn, pauseBtn, resumeBtn, stopBtn].forEach(b => b.disabled = true);
voiceSelect.disabled = true;
} else {
// populate voices (async; some browsers load voices after some time)
function loadVoices() {
voices = synth.getVoices().sort((a,b) => a.name.localeCompare(b.name));
voiceSelect.innerHTML = '';
voices.forEach((v,i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `${v.name} ${v.lang ? ' — ' + v.lang : ''}${v.default ? ' (default)' : ''}`;
voiceSelect.appendChild(opt);
});
// choose a default voice close to user's lang or the first
const userLang = navigator.language || navigator.userLanguage || '';
const preferredIndex = voices.findIndex(v => v.lang && v.lang.startsWith(userLang.split('-')[0]));
voiceSelect.value = preferredIndex >= 0 ? preferredIndex : 0;
setStatus('Voices loaded. Ready.');
}
loadVoices();
// Chrome/Edge may fire 'voiceschanged'
synth.onvoiceschanged = loadVoices;
}
// Sync UI labels for sliders
rateEl.addEventListener('input', () => rateVal.textContent = rateEl.value);
pitchEl.addEventListener('input', () => pitchVal.textContent = pitchEl.value);
volumeEl.addEventListener('input', () => volVal.textContent = volumeEl.value);
// Create a new utterance with current settings
function createUtterance() {
if (!synth) return null;
const text = textEl.value.trim();
if (!text) return null;
const u = new SpeechSynthesisUtterance(text);
const voiceIndex = parseInt(voiceSelect.value, 10);
if (!isNaN(voiceIndex) && voices[voiceIndex]) u.voice = voices[voiceIndex];
u.rate = Number(rateEl.value) || 1;
u.pitch = Number(pitchEl.value) || 1;
u.volume = Number(volumeEl.value);
// event handlers
u.onstart = () => {
setStatus('Speaking...');
playBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
resumeBtn.disabled = true;
};
u.onend = () => {
setStatus('Finished speaking.');
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
utter = null;
};
u.onerror = (e) => {
console.error('Speech error', e);
setStatus('Speech error occurred.', true);
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
utter = null;
};
u.onpause = () => {
setStatus('Paused.');
pauseBtn.disabled = true;
resumeBtn.disabled = false;
};
u.onresume = () => {
setStatus('Resumed.');
pauseBtn.disabled = false;
resumeBtn.disabled = true;
};
return u;
}
// Play
playBtn.addEventListener('click', () => {
if (!synth) return;
// If already speaking, stop first
if (synth.speaking) {
// stop current to restart with new settings
synth.cancel();
utter = null;
}
const u = createUtterance();
if (!u) {
setStatus('Please enter some text to speak.', true);
return;
}
utter = u;
// Some browsers require user gesture; we already are in click handler.
synth.speak(utter);
});
// Pause
pauseBtn.addEventListener('click', () => {
if (!synth) return;
if (synth.speaking && !synth.paused) {
synth.pause();
// onpause event will update UI
}
});
// Resume
resumeBtn.addEventListener('click', () => {
if (!synth) return;
if (synth.paused) {
synth.resume();
// onresume event will update UI
}
});
// Stop / Cancel
stopBtn.addEventListener('click', () => {
if (!synth) return;
if (synth.speaking) {
synth.cancel();
// onend/onerror will update UI
setStatus('Stopped.');
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
utter = null;
}
});
// Optional: update UI when user changes voice while speaking — restart speech
voiceSelect.addEventListener('change', () => {
if (!synth) return;
if (synth.speaking) {
// restart with new voice
synth.cancel();
const u = createUtterance();
if (u) {
utter = u;
synth.speak(utter);
}
}
});
// If user edits text while speaking, consider restarting (simple approach)
textEl.addEventListener('input', () => {
if (!synth) return;
if (synth.speaking) {
// small debounce to avoid too frequent restarts
if (window._ttsTimer) clearTimeout(window._ttsTimer);
window._ttsTimer = setTimeout(() => {
if (synth.speaking) {
synth.cancel();
const u = createUtterance();
if (u) synth.speak(u);
}
}, 500);
}
});
// Initialize controls state
(function initControls() {
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
})();
Subscribe
0 Comments
Oldest
Newest
Most Voted
Inline Feedbacks
View all comments
Related Projects
Day 16 : Scroll Progress Bar
Shows a progress bar at the top as the user scrolls down the page.
Concepts: scroll event, math calculations.
Day 20 : Music Player App
A mini music player with play, pause, next, and progress bar.
Concepts: Audio API, event listeners, state management.
Day 11 : Drum Kit
Play drum sounds when clicking buttons or pressing keys.
Concepts: Keyboard events, Audio API.