๐ ๏ธ The Ultimate Guide to Building the ๐ Instant Excuse Generator
Get ready for a deep dive! In this extremely descriptive tutorial, we tear down the exact JavaScript code and frameworks used to create our wildly popular ๐ Instant Excuse Generator tool. Grab a coffee, and let's get coding!
๐ฅ Why Create Utilities for Streamers?
Building small, interactive utilities like this is not only a fantastic way to learn JavaScript, but they are also highly sought after by content creators! Streamers on Twitch and YouTube are always looking for engaging ways to interact with their chat. By building web-based tools that can be embedded in OBS, you create massive value.
We design our tools to be OBS-compatible so streamers can easily integrate them into their overlays. You can check out our dedicated Streamers Page to see how our tools are optimized for broadcasting! Making things interactive builds communities.
๐ป Deep Dive: The JavaScript Engine
The ๐ Instant Excuse Generator looks simple on the outside, but underneath the hood lies a robust, reactive state management system powered by vanilla JavaScript. Let's break down the code to understand exactly how it works.
Section 1: Initial State & Properties
return {
soundEnabled: true,
isSpinning: false,
phase: 'idle', // 'idle', 'spin1', 'reject', 'spin2', 'final'
currentIndex: 0,
currentText: 'Spin the wheel!',
audioCtx: null,
excuses_en: [
"My cat locked me out and refused to negotiate. ๐ฑ",
"An alien satellite temporarily jammed my alarm clock. ๐ฝ",
"My Wi-Fi developed a personal emotional grudge. ๐ถ",
"I got trapped in a heated argument with my smart fridge. ๐ง",
"My GPS took me on a spiritual detour through the forest. ๐ฒ",
"I accidentally put my shoes on wrong and couldn't turn around. ๐",
"A rogue squirrel stole my car keys and hid in an oak tree. ๐ฟ๏ธ",
"My coffee was too hot, so I had to stare at it for 20 minutes. โ",
"I was busy defending my living room from an imaginary ghost. ๐ป",
"My sourdough starter demanded my undivided parental attention. ๐",
"I accidentally swallowed toothpaste and had to call a hotline. ๐ชฅ",
"A flock of ducks formed a strict blockade on my driveway. ๐ฆ",
"My alarm clock rang in a dream, so I woke up in 1998. โฐ",
"My socks didn't match the weather, so I had to reconsider life. ๐งฆ",
"I got stuck in a sweater for 45 minutes finding the armhole. ๐งฅ",
"My auto-correct sent an apology to the wrong universe. ๐ฑ",
"I was waiting for my roomba to finish its victory dance. ๐งน",
"A neighborhood cat held an urgent press conference on my porch. ๐ค",
"My horoscope strictly warned me against leaving the couch. ๐ฎ",
"I thought today was Sunday, so I was mentally on vacation. ๐
",
"My shoelaces tied themselves into a quantum knot. ๐",
"A mysterious forcefield prevented me from getting out of bed. ๐",
"I got distracted explaining gravity to my golden retriever. ๐",
"My microwave beeped at a frequency that paralyzed my legs. ๐ฟ",
"My brain was still installing a critical firmware update. ๐ง "
],
The Breakdown:
This section initializes the Alpine.js reactive state. Everything defined here becomes automatically available in the HTML. By setting sensible default values, we prevent the UI from loading in a broken or empty state.
Section 2: Application Logic Part 1
excuses_nl: [
"Mijn kat heeft me buitengesloten en wilde niet onderhandelen. ๐ฑ",
"Er stond een denkbeeldige file in mijn gang. ๐",
"Mijn Wi-Fi kreeg spontaan een persoonlijke woede-uitbarsting. ๐ถ",
"Ik raakte verstrikt in een felle discussie met mijn slimme koelkast. ๐ง",
"Mijn navigatie stuurde me op een spirituele zoektocht door het bos. ๐ฒ",
"Ik had mijn schoenen verkeerd om aan en kon geen bocht maken. ๐",
"Een eekhoorn ging er vandoor met mijn fietssleutel. ๐ฟ๏ธ",
"Mijn koffie was te heet, dus ik moest er 20 minuten naar staren. โ",
"Ik was druk bezig mijn woonkamer te verdedigen tegen een spook. ๐ป",
"Mijn zuurdesemstarter eiste dringende ouderlijke aandacht. ๐",
"Ik had per ongeluk tandpasta ingeslikt en moest de giflijn bellen. ๐ชฅ",
"Een koppel eenden hield een wegblokkade op mijn stoep. ๐ฆ",
"Mijn wekker ging af in mijn droom, dus ik werd wakker in 1998. โฐ",
"Mijn sokken matchen niet met het weer, dus ik moest herbezinnen. ๐งฆ",
"Ik zat 45 minuten vast in mijn trui op zoek naar het mouwgat. ๐งฅ",
"Mijn telefoon-autocorrectie stuurde een excuus naar het universum. ๐ฑ",
"Ik moest wachten tot mijn robotstofzuiger klaar was met zijn dansje. ๐งน",
"De buurtkat hield een dringende persconferentie op mijn mat. ๐ค",
"Mijn horoscoop verbood me uitdrukkelijk de bank te verlaten. ๐ฎ",
"Ik dacht oprecht dat het zondag was en zat al in de vakantiemodus. ๐
",
"Mijn veters knoopten zichzelf in een onontwarbare kwantumknoop. ๐",
"Er ontstond een mysterieus krachtveld rondom mijn warme dekbed. ๐",
"Ik raakte afgeleid toen ik zwaartekracht uitlegde aan mijn hond. ๐",
"Mijn magnetron piepte op een frequentie die mijn benen verlamde. ๐ฟ",
"Mijn brein was nog bezig met het installeren van een systeemupdate. ๐ง "
],
The Breakdown:
- This function handles specific business logic for the tool. It mutates the state (variables on `this`), which Alpine.js immediately detects, causing the HTML UI to re-render the changes instantly.
Section 3: Application Logic Part 2
list() {
let l = (document.documentElement && document.documentElement.getAttribute('lang')) || 'en';
return l === 'nl' ? this.excuses_nl : this.excuses_en;
},
playTickSound(pitch = 500, duration = 0.03) {
if (!this.soundEnabled) return;
try {
if (!this.audioCtx) {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.audioCtx.state === 'suspended') this.audioCtx.resume();
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(pitch, this.audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(120, this.audioCtx.currentTime + duration);
gain.gain.setValueAtTime(0.12, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(this.audioCtx.destination);
osc.start();
osc.stop(this.audioCtx.currentTime + duration);
} catch (e) {}
},
The Breakdown:
- This part handles the Web Audio API or HTML5 Audio elements, allowing us to play ticking sounds or victory fanfares to gamify the experience.
Section 4: Application Logic Part 3
playRejectSound() {
if (!this.soundEnabled) return;
try {
if (!this.audioCtx) return;
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, this.audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, this.audioCtx.currentTime + 0.25);
gain.gain.setValueAtTime(0.25, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.25);
osc.connect(gain);
gain.connect(this.audioCtx.destination);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.25);
} catch (e) {}
},
The Breakdown:
- This part handles the Web Audio API or HTML5 Audio elements, allowing us to play ticking sounds or victory fanfares to gamify the experience.
Section 5: Application Logic Part 4
playWinnerSound() {
if (!this.soundEnabled) return;
try {
if (!this.audioCtx) return;
const notes = [523.25, 659.25, 783.99, 1046.50];
notes.forEach((freq, i) => {
setTimeout(() => {
try {
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.15, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.25);
osc.connect(gain);
gain.connect(this.audioCtx.destination);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.25);
} catch (e) {}
}, i * 80);
});
} catch (e) {}
},
The Breakdown:
- Notice the use of
setTimeout. While JavaScript can calculate results instantly, adding a slight artificial delay creates suspense and makes the application feel like a physical game. - This part handles the Web Audio API or HTML5 Audio elements, allowing us to play ticking sounds or victory fanfares to gamify the experience.
Section 6: Application Logic Part 5
generate() {
if (this.isSpinning) return;
this.isSpinning = true;
this.phase = 'spin1';
let excuses = this.list();
let total = excuses.length;
let rejectIdx = Math.floor(Math.random() * total);
let finalIdx = (rejectIdx + 1 + Math.floor(Math.random() * (total - 1))) % total;
// --- PHASE 1: Rapid spin landing on rejected excuse ---
let delays1 = [45, 45, 45, 45, 45, 45, 45, 75, 120, 220, 400, 600];
let step1 = 0;
The Breakdown:
- We utilize
Math.random()here to introduce randomization. This is the core engine of the game's unpredictability, allowing us to shuffle arrays or pick random elements.
Section 7: Application Logic Part 6
const runStep1 = () => {
if (step1 < delays1.length - 1) {
this.currentIndex = (this.currentIndex + 1) % total;
this.currentText = excuses[this.currentIndex];
this.playTickSound(400 + step1 * 10);
let currentDelay = delays1[step1];
step1++;
setTimeout(runStep1, currentDelay);
} else {
// Land on Rejected Excuse!
this.currentIndex = rejectIdx;
this.currentText = excuses[rejectIdx];
this.phase = 'reject';
this.playRejectSound();
The Breakdown:
- Notice the use of
setTimeout. While JavaScript can calculate results instantly, adding a slight artificial delay creates suspense and makes the application feel like a physical game.
Section 8: Application Logic Part 7
// Pause 1400ms for dramatic rejection, then CONTINUE SPIN!
setTimeout(() => {
this.startSpin2(excuses, finalIdx);
}, 1400);
}
};
runStep1();
},
startSpin2(excuses, finalIdx) {
this.phase = 'spin2';
let total = excuses.length;
The Breakdown:
- Notice the use of
setTimeout. While JavaScript can calculate results instantly, adding a slight artificial delay creates suspense and makes the application feel like a physical game.
Section 9: Application Logic Part 8
// Spin 2 decelerates towards final winning excuse:
let delays2 = [250, 140, 80, 45, 45, 45, 75, 120, 200, 320, 500, 700];
let step2 = 0;
const runStep2 = () => {
if (step2 < delays2.length - 1) {
this.currentIndex = (this.currentIndex + 1) % total;
this.currentText = excuses[this.currentIndex];
this.playTickSound(500 + step2 * 12);
let currentDelay = delays2[step2];
step2++;
setTimeout(runStep2, currentDelay);
} else {
// Final land on Winner Excuse!
this.currentIndex = finalIdx;
this.currentText = excuses[finalIdx];
this.phase = 'final';
this.isSpinning = false;
this.playWinnerSound();
}
};
The Breakdown:
- Notice the use of
setTimeout. While JavaScript can calculate results instantly, adding a slight artificial delay creates suspense and makes the application feel like a physical game.
Section 10: Application Logic Part 9
runStep2();
}
}
}
The Breakdown:
- This function handles specific business logic for the tool. It mutates the state (variables on `this`), which Alpine.js immediately detects, causing the HTML UI to re-render the changes instantly.
๐ Frameworks Used & How to Link Them
Building modern web apps doesn't always require massive build tools like Webpack or Vite. For standalone utilities like ours, injecting powerful frameworks directly into the HTML via CDN (Content Delivery Network) is the fastest way to get started.
Alpine.js
Alpine is a rugged, minimal tool for composing behavior directly in your markup. It gives you the reactive and declarative nature of big frameworks like Vue or React at a fraction of the cost.
๐ External Link: Official Alpine.js Website
How to link in HTML:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Place this inside your <head> tag. The defer attribute ensures it loads without blocking your HTML rendering.
Tailwind CSS
Tailwind is a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
๐ External Link: Official Tailwind CSS Website
How to link in HTML (via CDN for prototyping):
<script src="https://cdn.tailwindcss.com"></script>
Place this in your <head> tag. While great for development and small utilities, for large production sites, you should install Tailwind via Node to purge unused CSS.
๐ Suggested Educational Resources
Want to learn more about the JavaScript concepts and frameworks used in this code? Check out these excellent free resources to level up your coding skills!
MDN Web Docs: JavaScript Guide
The ultimate, comprehensive guide to standard Vanilla JavaScript. Learn about variables, arrays, math functions, and DOM manipulation.
The Modern JavaScript Tutorial
A beautifully written, step-by-step tutorial covering modern ES6+ JavaScript concepts, closures, and promises.
freeCodeCamp: JS Algorithms
Get hands-on practice writing JavaScript logic (like our array shufflers and calculators) in this interactive coding curriculum.
โ FAQ about the Code
Is it hard to learn Alpine.js if I only know Vanilla JS?
Not at all! Alpine is designed to map directly to standard JavaScript. If you understand how a basic JS object and function work, you can pick up Alpine.js in literally one afternoon.
Why not use a database?
To ensure absolute privacy and lightning-fast speed, everything runs client-side in the browser. A database would add unnecessary overhead for these utility tools.
Can I copy this code for my own site?
Yes! We encourage you to build your own versions. Inspect the source code, see how the `x-data` object is structured, and try making modifications.