⚡ ZWRSK

🛠️ The Ultimate Guide to Building the 🚗 Designated Driver Picker

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 🚗 Designated Driver Picker 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 🚗 Designated Driver Picker 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', 'safe', 'speedup', 'spin2', 'bob_chosen'
                namesInput: 'Tom, Lisa, Daan, Anna',
                safePerson: '',
                finalBob: '',
                displayName: 'Spin the wheel!',
                audioCtx: null,
                // Twitch Chat Integration State
                showTwitchPanel: false,
                twitchChannel: '',
                twitchStatus: 'disconnected',
                twitchErrorMsg: '',
                twitchChatters: [],
                twitchClient: null,

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

                init() {
                    const params = new URLSearchParams(window.location.search);
                    if (params.has('channel')) {
                        this.twitchChannel = params.get('channel');
                        this.showTwitchPanel = true;
                        this.connectTwitch();
                    }
                },
                connectTwitch() {
                    if (!this.twitchChannel || this.twitchChannel.trim() === '') {
                        alert('Please enter a Twitch channel name!');
                        return;
                    }
                    const channel = this.twitchChannel.trim().toLowerCase().replace(/^#/, '');
                    this.twitchStatus = 'connecting';
                    this.twitchErrorMsg = '';

The Breakdown:

  • This is where the magic of Twitch integration happens. We use TMI.js to connect directly to a live Twitch chat and dynamically pull chatters' names into our state.

Section 3: Application Logic Part 2

                    if (this.twitchClient) {
                        try { this.twitchClient.disconnect(); } catch(e) {}
                    }
                    if (typeof tmi === 'undefined') {
                        this.twitchStatus = 'error';
                        this.twitchErrorMsg = 'TMI.js library not loaded';
                        return;
                    }
                    this.twitchClient = new tmi.Client({
                        options: { debug: false },
                        connection: { secure: true, reconnect: true },
                        channels: [channel]
                    });

The Breakdown:

  • This is where the magic of Twitch integration happens. We use TMI.js to connect directly to a live Twitch chat and dynamically pull chatters' names into our state.

Section 4: Application Logic Part 3

                    this.twitchClient.connect().then(() => {
                        this.twitchStatus = 'connected';
                    }).catch(err => {
                        this.twitchStatus = 'error';
                        this.twitchErrorMsg = 'Connection error: ' + (err ? (err.message || err) : 'Check channel name');
                    });
                    this.twitchClient.on('message', (target, tags, message, self) => {
                        if (self) return;
                        const msg = message.trim().toLowerCase();
                        if (msg === '!join' || msg.startsWith('!join ')) {
                            const name = tags['display-name'] || tags.username || 'Chatter';
                            this.addTwitchChatter(name);
                        }
                    });
                },

The Breakdown:

  • This is where the magic of Twitch integration happens. We use TMI.js to connect directly to a live Twitch chat and dynamically pull chatters' names into our state.

Section 5: Application Logic Part 4

                disconnectTwitch() {
                    if (this.twitchClient) {
                        try { this.twitchClient.disconnect(); } catch(e) {}
                        this.twitchClient = null;
                    }
                    this.twitchStatus = 'disconnected';
                },
                addTwitchChatter(name) {
                    let existing = this.namesInput.split(',').map(n => n.trim()).filter(n => n.length > 0);
                    if (!existing.some(n => n.toLowerCase() === name.toLowerCase())) {
                        if (existing.length === 0) {
                            this.namesInput = name;
                        } else {
                            this.namesInput = this.namesInput + ', ' + name;
                        }
                        if (!this.twitchChatters.includes(name)) {
                            this.twitchChatters.push(name);
                        }
                        this.playTickSound(600);
                    }
                },

The Breakdown:

  • Here, we process the raw string input from the user. Using chained array methods like .split(), .map(), and .filter() cleans up whitespace and removes empty entries effortlessly.
  • This is where the magic of Twitch integration happens. We use TMI.js to connect directly to a live Twitch chat and dynamically pull chatters' names into our state.

Section 6: Application Logic Part 5

                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 7: Application Logic Part 6

                playSafeSound() {
                    if (!this.soundEnabled) return;
                    try {
                        if (!this.audioCtx) return;
                        const notes = [523.25, 659.25, 783.99]; // C5-E5-G5
                        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.2);
                                    osc.connect(gain);
                                    gain.connect(this.audioCtx.destination);
                                    osc.start();
                                    osc.stop(this.audioCtx.currentTime + 0.2);
                                } 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 8: Application Logic Part 7

                playRevUpSound() {
                    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(150, this.audioCtx.currentTime);
                        osc.frequency.exponentialRampToValueAtTime(600, this.audioCtx.currentTime + 0.5);
                        gain.gain.setValueAtTime(0.15, this.audioCtx.currentTime);
                        gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.5);
                        osc.connect(gain);
                        gain.connect(this.audioCtx.destination);
                        osc.start();
                        osc.stop(this.audioCtx.currentTime + 0.5);
                    } 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 9: Application Logic Part 8

                playBobSound() {
                    if (!this.soundEnabled) return;
                    try {
                        if (!this.audioCtx) return;
                        [349.23, 415.30].forEach(freq => {
                            try {
                                const osc = this.audioCtx.createOscillator();
                                const gain = this.audioCtx.createGain();
                                osc.type = 'square';
                                osc.frequency.value = freq;
                                gain.gain.setValueAtTime(0.2, this.audioCtx.currentTime);
                                gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.4);
                                osc.connect(gain);
                                gain.connect(this.audioCtx.destination);
                                osc.start();
                                osc.stop(this.audioCtx.currentTime + 0.4);
                            } catch (e) {}
                        });
                    } 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 10: Application Logic Part 9

                spin() {
                    if (this.isSpinning) return;
                    let names = this.namesInput.split(',').map(n => n.trim()).filter(n => n.length > 0);
                    let isNl = (document.documentElement && document.documentElement.getAttribute('lang')) === 'nl';
                    if (names.length < 2) {
                        let msg = isNl
                            ? 'Voer ten minste 2 namen in om een Bob te kiezen!'
                            : 'Please enter at least 2 passenger names to pick a driver!';
                        alert(msg);
                        return;
                    }

The Breakdown:

  • Here, we process the raw string input from the user. Using chained array methods like .split(), .map(), and .filter() cleans up whitespace and removes empty entries effortlessly.

Section 11: Application Logic Part 10

                    this.isSpinning = true;
                    this.phase = 'spin1';
                    // Pick Safe Person and Final Bob Person
                    let safeIdx = Math.floor(Math.random() * names.length);
                    let remainingCandidates = names.filter((_, idx) => idx !== safeIdx);
                    let bobIdx = Math.floor(Math.random() * remainingCandidates.length);
                    this.safePerson = names[safeIdx];
                    this.finalBob = remainingCandidates[bobIdx];
                    // --- PHASE 1: SPIN & LAND ON SAFE PERSON ---
                    let delays1 = [45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 75, 75, 120, 180, 280, 450, 650];
                    let idx1 = 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.
  • Here, we process the raw string input from the user. Using chained array methods like .split(), .map(), and .filter() cleans up whitespace and removes empty entries effortlessly.

Section 12: Application Logic Part 11

                    const runPhase1 = () => {
                        if (idx1 < delays1.length - 1) {
                            let tempName = names[idx1 % names.length];
                            this.displayName = tempName;
                            this.playTickSound(400 + (idx1 * 10));
                            let currentDelay = delays1[idx1];
                            idx1++;
                            setTimeout(runPhase1, currentDelay);
                        } else {
                            // Land on Safe Person!
                            this.displayName = this.safePerson;
                            this.phase = 'safe';
                            this.playSafeSound();

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 13: Application Logic Part 12

                            // Pause 1500ms for dramatic effect, then RE-ACCELERATE!
                            setTimeout(() => {
                                this.startReAcceleration(names, remainingCandidates);
                            }, 1500);
                        }
                    };
                    runPhase1();
                },
                startReAcceleration(allNames, candidates) {
                    this.phase = 'speedup';
                    this.playRevUpSound();

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 14: Application Logic Part 13

                    // Re-acceleration delays going fast again:
                    let revDelays = [350, 200, 120, 70, 45];
                    let revIdx = 0;
                    const runRev = () => {
                        if (revIdx < revDelays.length) {
                            this.displayName = candidates[revIdx % candidates.length];
                            let currentDelay = revDelays[revIdx];
                            revIdx++;
                            setTimeout(runRev, currentDelay);
                        } else {
                            // --- PHASE 2: RAPID SECOND SPIN TOWARDS FINAL BOB ---
                            this.phase = 'spin2';
                            this.runPhase2(candidates);
                        }
                    };

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 15: Application Logic Part 14

                    runRev();
                },
                runPhase2(candidates) {
                    let delays2 = [45, 45, 45, 45, 45, 45, 45, 45, 75, 110, 180, 260, 420, 650];
                    let idx2 = 0;
                    const runPhase2Step = () => {
                        if (idx2 < delays2.length - 1) {
                            this.displayName = candidates[idx2 % candidates.length];
                            this.playTickSound(500 + (idx2 * 12));
                            let currentDelay = delays2[idx2];
                            idx2++;
                            setTimeout(runPhase2Step, currentDelay);
                        } else {
                            // Final Land on BOB!
                            this.displayName = this.finalBob;
                            this.phase = 'bob_chosen';
                            this.isSpinning = false;
                            this.playBobSound();
                        }
                    };

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 16: Application Logic Part 15

                    runPhase2Step();
                }
            }
        }

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!

❓ 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.

⬅️ Previous: Fair Tikkie & Bill Splitter Back to Utility Tool Next: Digital Straw Picker ➡️