One of the best ways to get comfortable with a new set of tools is to build something small and functional with them. This guide walks through building a Fun Alphabet Reader — a web app where clicking any letter triggers the browser to read it aloud using the built-in Speech Synthesis API.
The finished project is live at funabc.nixx.dev if you want to see what you are working toward.
What you will build and learn:
Setting up a Vite project with TypeScript
Integrating Tailwind CSS and SCSS for styling
Using the browser's Speech Synthesis API for text-to-speech
Organizing a small frontend project cleanly from the start
Step 1: Set Up the Vite Project
Create a new project using the Vite CLI:
npm create vite@latestWhen prompted, name the project fun-alphabet-reader, select Vanilla as the framework, and choose TypeScript as the variant. Then install dependencies:
cd fun-alphabet-reader
npm installStep 2: Install Tailwind CSS and SCSS Support
Install Tailwind and its required tooling:
npm install -D tailwindcss postcss autoprefixer @tailwindcss/viteThen add SCSS support:
npm install -D sass-embeddedTailwind handles utility-based styling. SCSS adds nesting and mixins, which keeps the button styles readable as the project grows.
Step 3: Organize the Project Structure
Clear out the default src/ contents (keep vite-env.d.ts) and set up the following structure:
fun-alphabet-reader/
├── public/
├── src/
│ ├── app.ts # Core logic
│ ├── main.ts # Entry point
│ └── index.scss # Styling
├── index.html
├── vite.config.ts
├── package.json
└── README.mdThis keeps concerns separated from the start — HTML structure in index.html, logic in app.ts, and styles in index.scss.
Step 4: Define the HTML Structure
Update index.html with the base layout and meta tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Interactive alphabet reader using speech synthesis." />
<title>Fun Alphabet Reader</title>
</head>
<body class="bg-slate-50 text-gray-800">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>The #app div is the mount point. Everything else gets injected by TypeScript at runtime.
Step 5: Style with Tailwind CSS and SCSS
Create src/index.scss:
@use 'tailwindcss' as *;
@mixin button-hover-focus {
@apply bg-lime-400 shadow-none ring-red-500 ring-1 ring-offset-2;
}
.alphabet-container {
button {
@apply text-sm sm:text-base rounded-md bg-lime-500 text-lime-50 font-medium py-1 px-2 focus:outline-none transition-colors duration-300 cursor-pointer shadow-sm;
&:hover,
&:focus {
@include button-hover-focus;
}
}
}The @mixin keeps hover and focus styles in one place. If the design changes later, one update covers both states.
Step 6: Create the Entry Point
Create src/main.ts:
import { createContainerElement, runApp } from './app';
import './index.scss';
(() => {
const app = document.getElementById('app');
if (app) {
app.appendChild(createContainerElement());
runApp();
}
})();This file does one thing: find the mount point, attach the UI, and start the app. Keeping it minimal makes the entry point easy to follow at a glance.
Step 7: Implement the Core Logic
Create src/app.ts:
export const createContainerElement = (): HTMLDivElement => {
const container = document.createElement('div');
container.className = 'container max-w-lg mx-auto p-3 sm:p-10';
container.innerHTML = `
<h1 class="text-2xl sm:text-3xl font-bold mb-0.5">
<span class="text-slate-500 border-b-3 border-double">Fun Alphabet</span> Reader
</h1>
<p class="text-sm sm:text-base">Click a letter to hear it!</p>
<div class="flex justify-end my-5">
<select class="text-sm focus:outline-none focus:ring-1 rounded-md" id="voice-select"></select>
</div>
<div id="alphabet-container" class="alphabet-container grid grid-cols-8 gap-5 mt-5"></div>
`;
return container;
};
export const runApp = () => {
const synth = window.speechSynthesis;
let voices: SpeechSynthesisVoice[] = [];
function loadVoices() {
voices = synth.getVoices();
}
function createAlphabetButtons() {
[...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'].forEach(letter => {
const button = document.createElement('button');
button.textContent = letter;
button.onclick = () => synth.speak(new SpeechSynthesisUtterance(letter));
document.getElementById('alphabet-container')?.appendChild(button);
});
}
createAlphabetButtons();
synth.onvoiceschanged = loadVoices;
loadVoices();
};A few things worth noting here:
createContainerElementbuilds the UI structure and returns a DOM node, keeping it testable and independent of the mount logic inmain.tsThe spread syntax
[...'ABCDEFGHIJKLMNOPQRSTUVWXYZ']splits the string into individual characters cleanlySpeechSynthesisUtteranceis a standard browser API — no external library neededThe voice select dropdown is wired up and ready for a future feature that lets users choose between available voices
Where to Take It Next
The app is functional as-is, but a few natural extensions are worth exploring once the basics are comfortable:
Voice selection: The
voicesarray is already populated. Connect it to the#voice-selectdropdown and pass the chosen voice to theSpeechSynthesisUtteranceinstanceCustom words: Add an input field that lets users type a word and hear it spoken in full
Animations: Use Tailwind's transition utilities to animate a letter when it is clicked
Key Takeaways
Vite provides a fast, minimal setup for TypeScript projects without manual Webpack configuration.
Tailwind CSS with SCSS gives you utility-first styling alongside the ability to use mixins and nesting where needed.
The Speech Synthesis API is built into modern browsers and requires no external dependency.
Separating UI construction (
createContainerElement) from runtime logic (runApp) keeps both functions easier to read and maintain.TypeScript catches type errors at compile time, which is especially useful when working with browser APIs that return typed objects like
SpeechSynthesisVoice.
Conclusion
This project covers more ground than it might appear at first. In the process of building a simple alphabet reader, you set up a modern build pipeline, integrated a CSS framework, and used a browser API that is genuinely useful for accessibility features in production applications.
The source code is available on GitHub if you want to compare your implementation or start from the completed version.
Built an extension or ran into a setup issue? Share it in the comments.




