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-live status messages.
  • Graceful note if speechSynthesis is 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
Notify of
guest
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.