Skip to main content

⏱️ Lesson 18: Timers & Animation

JavaScript doesn't just respond to clicks — it can schedule code to run later, repeat at intervals, and sync with the browser's rendering engine. In this lesson you'll learn the timing APIs that power countdowns, auto-saves, live clocks, and smooth animations.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Delay code execution with setTimeout and cancel it with clearTimeout
  • Run code at regular intervals with setInterval and stop it with clearInterval
  • Build practical features like countdowns, debounce, and auto-save
  • Create smooth animations with requestAnimationFrame
  • Trigger CSS transitions from JavaScript by toggling classes
  • Understand the event loop and how timers actually work under the hood

Estimated Time: 50 minutes

Project: Build a Pomodoro timer with animated progress

📑 In This Lesson

setTimeout — Run Once, Later

setTimeout schedules a function to run once after a specified delay (in milliseconds). It returns an ID you can use to cancel the timer before it fires.


// Run a function after 2 seconds (2000ms)
setTimeout(() => {
    console.log("This runs after 2 seconds");
}, 2000);

// With a named function
function showWelcome() {
    console.log("Welcome to the app!");
}
setTimeout(showWelcome, 3000);  // 3 seconds
                

Canceling a Timeout

setTimeout returns a numeric ID. Pass it to clearTimeout to cancel the scheduled function before it runs.


// Schedule a timeout
const timerId = setTimeout(() => {
    alert("Time's up!");
}, 5000);

// Cancel it before it fires
const cancelBtn = document.querySelector("#cancel");
cancelBtn.addEventListener("click", () => {
    clearTimeout(timerId);
    console.log("Timer canceled!");
});
                

Passing Arguments


// Extra arguments are passed to the callback
setTimeout((name, role) => {
    console.log(`Hello ${name}, you are a ${role}`);
}, 1000, "Ray", "developer");

// Equivalent to:
setTimeout(() => {
    greet("Ray", "developer");
}, 1000);
                

⚠️ Common Mistake: Calling Instead of Passing


// ❌ WRONG — this calls the function immediately!
setTimeout(showWelcome(), 2000);

// ✅ RIGHT — pass the function reference
setTimeout(showWelcome, 2000);

// ✅ Also right — wrap in an arrow function
setTimeout(() => showWelcome(), 2000);
                    

Don't add parentheses () when passing a function to setTimeout. Parentheses call the function right away; you want to pass a reference for setTimeout to call later.

setInterval — Run Repeatedly

setInterval runs a function repeatedly at a fixed interval. Like setTimeout, it returns an ID you use with clearInterval to stop it.


// Update a clock every second
const clockDisplay = document.querySelector("#clock");

const intervalId = setInterval(() => {
    const now = new Date();
    clockDisplay.textContent = now.toLocaleTimeString();
}, 1000);

// Stop the clock
document.querySelector("#stop-clock").addEventListener("click", () => {
    clearInterval(intervalId);
});
                

Building a Countdown


function startCountdown(seconds, display) {
    let remaining = seconds;
    display.textContent = remaining;

    const intervalId = setInterval(() => {
        remaining--;
        display.textContent = remaining;

        if (remaining <= 0) {
            clearInterval(intervalId);
            display.textContent = "Done!";
        }
    }, 1000);

    return intervalId;  // Return so caller can cancel if needed
}

const display = document.querySelector("#countdown");
const timerId = startCountdown(10, display);
                

setTimeout vs. setInterval

Feature setTimeout setInterval
How oftenOnceRepeatedly
Cancel withclearTimeout(id)clearInterval(id)
Use forDelays, debounce, one-shotClocks, polling, animations
Drift riskN/AYes — intervals can drift over time

Recursive setTimeout — A Better Interval

setInterval fires every N milliseconds regardless of how long the callback takes. If the callback is slow (e.g., a network request), calls can stack up. A recursive setTimeout waits for each execution to finish before scheduling the next.


// setInterval — fires every 2s even if the callback is slow
setInterval(async () => {
    await fetchData();  // Takes 1.5s
    updateUI();         // Next call fires 0.5s after this finishes
}, 2000);

// Recursive setTimeout — waits for completion, then schedules
async function poll() {
    await fetchData();  // Takes 1.5s
    updateUI();
    setTimeout(poll, 2000);  // 2s AFTER this finishes
}
poll();  // Start the cycle
                
graph LR subgraph setInterval direction LR A1["Call"] -->|2s| A2["Call"] A2 -->|2s| A3["Call"] A3 -->|2s| A4["Call"] end subgraph "Recursive setTimeout" direction LR B1["Call"] -->|"done + 2s"| B2["Call"] B2 -->|"done + 2s"| B3["Call"] end style A1 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style A2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style A3 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style A4 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style B1 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style B2 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style B3 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b

Practical Timer Patterns

Timers aren't just for countdowns. Here are three real-world patterns you'll use constantly.

Debounce — Wait Until the User Stops

Debouncing delays a function until the user stops doing something (like typing). Each new event resets the timer. This is perfect for search-as-you-type, where you don't want to fire a request on every single keystroke.


function debounce(fn, delay) {
    let timerId;
    return (...args) => {
        clearTimeout(timerId);
        timerId = setTimeout(() => fn(...args), delay);
    };
}

// Usage: search only after user stops typing for 300ms
const searchInput = document.querySelector("#search");
const handleSearch = debounce((event) => {
    console.log("Searching for:", event.target.value);
    // fetch(`/api/search?q=${event.target.value}`)...
}, 300);

searchInput.addEventListener("input", handleSearch);
                

Throttle — Limit How Often It Runs

Throttling ensures a function runs at most once per time window, no matter how often the event fires. Great for scroll and resize handlers.


function throttle(fn, limit) {
    let waiting = false;
    return (...args) => {
        if (!waiting) {
            fn(...args);
            waiting = true;
            setTimeout(() => { waiting = false; }, limit);
        }
    };
}

// Usage: handle scroll at most every 200ms
window.addEventListener("scroll", throttle(() => {
    console.log("Scroll position:", window.scrollY);
}, 200));
                

Auto-Save with Debounce


const editor = document.querySelector("#editor");
const statusEl = document.querySelector("#save-status");

const autoSave = debounce((content) => {
    // Save to localStorage (or send to server)
    localStorage.setItem("draft", content);
    statusEl.textContent = "Saved!";
    setTimeout(() => { statusEl.textContent = ""; }, 2000);
}, 1000);

editor.addEventListener("input", (event) => {
    statusEl.textContent = "Saving...";
    autoSave(event.target.value);
});
                
graph TD subgraph Debounce D1["Keystroke"] --> D2["Reset timer"] D2 --> D3["Keystroke"] --> D4["Reset timer"] D4 --> D5["...silence..."] D5 --> D6["Timer fires!
Execute function"] end subgraph Throttle T1["Event fires"] --> T2["Execute function"] T2 --> T3["Lock for N ms"] T3 --> T4["Events ignored"] T4 --> T5["Unlock"] T5 --> T6["Next event executes"] end style D1 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D3 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D6 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style T1 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style T2 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style T4 fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style T6 fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style D2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style D4 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style D5 fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b style T3 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style T5 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b

💡 When to Debounce vs. Throttle

  • Debounce when you only care about the final value: search input, form auto-save, window resize calculations
  • Throttle when you need periodic updates during continuous activity: scroll position tracking, mouse move effects, rate-limited API calls

How Timers Really Work: The Event Loop

JavaScript is single-threaded — it can only do one thing at a time. So how do timers work without freezing the page? The answer is the event loop.


console.log("1: Start");

setTimeout(() => {
    console.log("2: Timeout callback");
}, 0);  // 0ms delay!

console.log("3: End");

// Output:
// 1: Start
// 3: End
// 2: Timeout callback  ← runs AFTER current code finishes
                

Even with a delay of 0, the timeout callback doesn't run immediately. It's placed in a task queue and only executes after the current code (the "call stack") finishes. This is the event loop in action.

graph TD A["Call Stack
(current code)"] --> B{"Stack empty?"} B -->|No| A B -->|Yes| C["Event Loop
checks queues"] C --> D["Task Queue
(setTimeout, setInterval,
click handlers)"] D --> E["Move next task
to Call Stack"] E --> A style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style C fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style E fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b

⚠️ Timer Delays Are Minimums, Not Guarantees

setTimeout(fn, 500) means "run this no sooner than 500ms from now." If the call stack is busy (heavy computation, long-running loop), the callback will wait. The browser also enforces a minimum delay of about 4ms for nested timers. For precise timing (animations, games), use requestAnimationFrame instead.

Why This Matters


// ❌ This will freeze the page — blocking the call stack
for (let i = 0; i < 1000000000; i++) {
    // Heavy computation — nothing else can run
}

// ✅ Break heavy work into chunks with setTimeout
function processChunk(data, index, chunkSize) {
    const end = Math.min(index + chunkSize, data.length);
    for (let i = index; i < end; i++) {
        // Process item
    }
    if (end < data.length) {
        // Schedule next chunk — lets other events run between chunks
        setTimeout(() => processChunk(data, end, chunkSize), 0);
    }
}
                

requestAnimationFrame — Smooth Animation

requestAnimationFrame (rAF) tells the browser: "Call this function right before the next screen repaint." It runs at the display's refresh rate — usually 60 times per second (60 fps), giving you ~16.7ms per frame. This is the right tool for visual animations.

Basic Animation Loop


const box = document.querySelector(".animated-box");
let position = 0;

function animate() {
    position += 2;  // Move 2px per frame
    box.style.transform = `translateX(${position}px)`;

    if (position < 400) {
        requestAnimationFrame(animate);  // Schedule next frame
    }
}

requestAnimationFrame(animate);  // Start the animation
                

Using the Timestamp

rAF passes a high-resolution timestamp to your callback. Use it for frame-rate-independent animation — so the motion looks the same whether the browser runs at 30 fps or 144 fps.


const box = document.querySelector(".animated-box");
const duration = 2000;  // 2 seconds
let startTime = null;

function animate(timestamp) {
    if (!startTime) startTime = timestamp;
    const elapsed = timestamp - startTime;

    // Progress from 0 to 1 over the duration
    const progress = Math.min(elapsed / duration, 1);

    // Move from 0 to 400px
    box.style.transform = `translateX(${progress * 400}px)`;

    if (progress < 1) {
        requestAnimationFrame(animate);
    }
}

requestAnimationFrame(animate);
                

Canceling an Animation


let animationId;

function animate() {
    // ... animation logic ...
    animationId = requestAnimationFrame(animate);
}

// Start
animationId = requestAnimationFrame(animate);

// Stop
cancelAnimationFrame(animationId);
                

Why Not setInterval for Animations?

Factor setInterval requestAnimationFrame
Syncs with displayNo — can cause jankYes — smooth frames
Background tabsKeeps running (wastes resources)Pauses automatically
Frame rateFixed (you pick ms)Matches monitor (60/120/144 fps)
Battery impactHigherLower (pauses when hidden)
Best forNon-visual timers, pollingVisual animations

💡 Easing Functions

Linear motion (constant speed) looks robotic. Easing functions make animations feel natural by varying the speed over time.


// Linear: constant speed
const linear = t => t;

// Ease out: fast start, slow finish
const easeOut = t => 1 - Math.pow(1 - t, 3);

// Ease in-out: slow start and end, fast middle
const easeInOut = t => t < 0.5
    ? 4 * t * t * t
    : 1 - Math.pow(-2 * t + 2, 3) / 2;

// Usage with rAF
const progress = elapsed / duration;
const eased = easeOut(progress);
box.style.transform = `translateX(${eased * 400}px)`;
                    

CSS Transitions via JavaScript

For many animations, you don't need requestAnimationFrame at all. CSS transitions handle the animation automatically — JavaScript just triggers them by changing a property or toggling a class.

The Pattern: CSS Defines the Animation, JS Triggers It


<style>
    .box {
        width: 100px;
        height: 100px;
        background: #3b82f6;
        transform: translateX(0);
        opacity: 1;
        /* CSS transition — the browser animates these properties */
        transition: transform 0.5s ease, opacity 0.3s ease;
    }

    .box.slide-right {
        transform: translateX(300px);
    }

    .box.fade-out {
        opacity: 0;
    }
</style>
                

const box = document.querySelector(".box");
const slideBtn = document.querySelector("#slide");
const fadeBtn = document.querySelector("#fade");

// Toggle the class — CSS handles the animation
slideBtn.addEventListener("click", () => {
    box.classList.toggle("slide-right");
});

fadeBtn.addEventListener("click", () => {
    box.classList.toggle("fade-out");
});
                

Listening for Transition End

The transitionend event fires when a CSS transition finishes. Use it to chain actions or clean up.


const notification = document.querySelector(".notification");

// Fade out, then remove from DOM
function dismissNotification(el) {
    el.classList.add("fade-out");

    el.addEventListener("transitionend", () => {
        el.remove();
    }, { once: true });  // Auto-remove the listener
}

dismissNotification(notification);
                

Triggering Transitions on Dynamic Elements

New elements start with their final styles — there's no "before" state for the browser to transition from. You need to force a layout calculation (a "reflow") between setting the initial state and the final state.


const toast = document.createElement("div");
toast.classList.add("toast");  // Has transition: opacity 0.3s
toast.style.opacity = "0";    // Start invisible
toast.textContent = "Saved!";
document.body.append(toast);

// Force reflow — the browser now "knows" opacity is 0
toast.offsetHeight;  // Reading any layout property triggers reflow

// NOW set the final state — the transition runs
toast.style.opacity = "1";

// Auto-dismiss after 3 seconds
setTimeout(() => {
    toast.style.opacity = "0";
    toast.addEventListener("transitionend", () => toast.remove(), { once: true });
}, 3000);
                

✅ CSS vs. JavaScript Animation — Rules of Thumb

  • Use CSS transitions for simple state changes: hover effects, show/hide, slide in/out, color changes
  • Use CSS @keyframes for looping or complex multi-step animations that don't depend on user input
  • Use requestAnimationFrame for animations that depend on dynamic values (mouse position, game physics, canvas drawing)
  • Prefer CSS when possible — the browser can optimize CSS animations on the GPU, and they don't block the main thread

Hands-on Exercise

🏋️ Exercise: Pomodoro Timer with Animated Progress

Objective: Build a Pomodoro-style countdown timer with start, pause, and reset controls, a circular progress indicator, and a notification when time is up.


<!-- Save this as pomodoro.html and open in a browser -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Pomodoro Timer</title>
    <style>
        * { box-sizing: border-box; margin: 0; }
        body {
            font-family: system-ui, sans-serif; background: #0f172a;
            color: #e2e8f0; display: flex; justify-content: center;
            align-items: center; min-height: 100vh;
        }
        .timer-container { text-align: center; }
        h1 { margin-bottom: 2rem; font-size: 1.5rem; color: #94a3b8; }

        .timer-ring {
            width: 280px; height: 280px; margin: 0 auto 2rem;
            position: relative;
        }
        .timer-ring svg { transform: rotate(-90deg); }
        .timer-ring circle {
            fill: none; stroke-width: 8;
        }
        .bg-ring { stroke: #1e293b; }
        .progress-ring {
            stroke: #6366f1;
            stroke-linecap: round;
            transition: stroke-dashoffset 0.5s ease;
        }
        .timer-display {
            position: absolute; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            font-size: 3.5rem; font-weight: 700;
            font-variant-numeric: tabular-nums;
        }
        .timer-label {
            position: absolute; bottom: 55px; left: 50%;
            transform: translateX(-50%);
            font-size: 0.9rem; color: #94a3b8;
        }

        .controls { display: flex; gap: 1rem; justify-content: center; margin-bottom: 1.5rem; }
        .controls button {
            padding: 0.6rem 1.5rem; border: 2px solid #334155;
            border-radius: 8px; background: #1e293b; color: #e2e8f0;
            font-size: 1rem; cursor: pointer; transition: all 0.2s;
        }
        .controls button:hover { border-color: #6366f1; background: #334155; }
        .controls button.active { background: #6366f1; border-color: #6366f1; }

        .presets { display: flex; gap: 0.5rem; justify-content: center; }
        .presets button {
            padding: 0.4rem 1rem; border: 1px solid #334155; border-radius: 6px;
            background: transparent; color: #94a3b8; cursor: pointer;
            font-size: 0.85rem; transition: all 0.2s;
        }
        .presets button:hover { color: #e2e8f0; border-color: #6366f1; }
        .presets button.selected { background: #334155; color: #e2e8f0; border-color: #6366f1; }

        .done-message {
            font-size: 1.2rem; color: #22c55e; font-weight: 600;
            opacity: 0; transition: opacity 0.5s;
        }
        .done-message.visible { opacity: 1; }
    </style>
</head>
<body>
    <div class="timer-container">
        <h1>🍅 Pomodoro Timer</h1>

        <div class="timer-ring">
            <svg width="280" height="280">
                <circle class="bg-ring" cx="140" cy="140" r="130"></circle>
                <circle class="progress-ring" id="progress-ring"
                        cx="140" cy="140" r="130"
                        stroke-dasharray="816.81"
                        stroke-dashoffset="0"></circle>
            </svg>
            <div class="timer-display" id="timer-display">25:00</div>
            <div class="timer-label" id="timer-label">Focus Time</div>
        </div>

        <div class="controls">
            <button id="start-btn">▶ Start</button>
            <button id="pause-btn">⏸ Pause</button>
            <button id="reset-btn">↺ Reset</button>
        </div>

        <div class="presets">
            <button class="preset-btn selected" data-minutes="25">25 min</button>
            <button class="preset-btn" data-minutes="15">15 min</button>
            <button class="preset-btn" data-minutes="5">5 min</button>
            <button class="preset-btn" data-minutes="1">1 min</button>
        </div>

        <p class="done-message" id="done-message">🎉 Time's up! Great work!</p>
    </div>

    <script>
    // TODO: Complete each task

    // The circumference of the circle (2 * π * r where r = 130)
    const CIRCUMFERENCE = 2 * Math.PI * 130;  // ~816.81

    // 1. Get DOM references:
    //    - #progress-ring, #timer-display, #timer-label, #done-message
    //    - #start-btn, #pause-btn, #reset-btn
    //    - All .preset-btn buttons

    // 2. Set up state variables:
    //    - totalSeconds (default 25 * 60)
    //    - remainingSeconds (starts equal to totalSeconds)
    //    - intervalId (null when not running)
    //    - isRunning (boolean)

    // 3. Write updateDisplay(seconds):
    //    - Convert seconds to MM:SS format
    //    - Update #timer-display text
    //    - Calculate progress (0 to 1) and update
    //      stroke-dashoffset on #progress-ring
    //      Formula: CIRCUMFERENCE * (1 - remaining / total)

    // 4. Write start():
    //    - If already running, return
    //    - Set isRunning = true, hide done message
    //    - Use setInterval (1000ms) to decrement remainingSeconds
    //    - Call updateDisplay each tick
    //    - When remainingSeconds hits 0, clearInterval,
    //      show done message, mark not running

    // 5. Write pause() and reset():
    //    - pause: clearInterval, set isRunning = false
    //    - reset: pause, set remainingSeconds = totalSeconds,
    //      updateDisplay, hide done message

    // 6. Hook up buttons:
    //    - Start → start()
    //    - Pause → pause()
    //    - Reset → reset()

    // 7. Hook up preset buttons:
    //    - On click: set totalSeconds and remainingSeconds
    //    - Reset display, highlight selected preset
    //    - Pause if running

    // 8. Bonus: Change the ring color to red when < 30 seconds remain
    //    and to green when timer completes
    </script>
</body>
</html>
                    
💡 Hint

3: To format time: String(minutes).padStart(2, "0") ensures "05" instead of "5". For the ring, stroke-dashoffset controls how much of the circle is hidden — 0 = full, CIRCUMFERENCE = empty.

4: Remember to store the interval ID so you can clear it later. Decrement first, then check if it's 0, then update display.

7: Use event delegation on the presets container. Read data-minutes from the clicked button. Remove selected from all presets, add it to the clicked one.

8: Inside updateDisplay, check remainingSeconds < 30 and change progressRing.style.stroke.

✅ Solution

const CIRCUMFERENCE = 2 * Math.PI * 130;

// 1. DOM references
const progressRing = document.querySelector("#progress-ring");
const timerDisplay = document.querySelector("#timer-display");
const timerLabel = document.querySelector("#timer-label");
const doneMessage = document.querySelector("#done-message");
const startBtn = document.querySelector("#start-btn");
const pauseBtn = document.querySelector("#pause-btn");
const resetBtn = document.querySelector("#reset-btn");
const presetBtns = document.querySelectorAll(".preset-btn");

// 2. State
let totalSeconds = 25 * 60;
let remainingSeconds = totalSeconds;
let intervalId = null;
let isRunning = false;

// 3. Update the display
function updateDisplay(seconds) {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    timerDisplay.textContent =
        `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;

    // Update ring progress
    const progress = 1 - (seconds / totalSeconds);
    const offset = CIRCUMFERENCE * progress;
    progressRing.style.strokeDashoffset = offset;

    // 8. Bonus: color changes
    if (seconds === 0) {
        progressRing.style.stroke = "#22c55e";
    } else if (seconds < 30) {
        progressRing.style.stroke = "#ef4444";
    } else {
        progressRing.style.stroke = "#6366f1";
    }
}

// 4. Start the timer
function start() {
    if (isRunning) return;
    if (remainingSeconds <= 0) return;

    isRunning = true;
    doneMessage.classList.remove("visible");

    intervalId = setInterval(() => {
        remainingSeconds--;
        updateDisplay(remainingSeconds);

        if (remainingSeconds <= 0) {
            clearInterval(intervalId);
            intervalId = null;
            isRunning = false;
            doneMessage.classList.add("visible");
        }
    }, 1000);
}

// 5. Pause and reset
function pause() {
    clearInterval(intervalId);
    intervalId = null;
    isRunning = false;
}

function reset() {
    pause();
    remainingSeconds = totalSeconds;
    updateDisplay(remainingSeconds);
    doneMessage.classList.remove("visible");
}

// 6. Button handlers
startBtn.addEventListener("click", start);
pauseBtn.addEventListener("click", pause);
resetBtn.addEventListener("click", reset);

// 7. Preset buttons
presetBtns.forEach(btn => {
    btn.addEventListener("click", () => {
        presetBtns.forEach(b => b.classList.remove("selected"));
        btn.classList.add("selected");

        totalSeconds = Number(btn.dataset.minutes) * 60;
        remainingSeconds = totalSeconds;
        pause();
        updateDisplay(remainingSeconds);
        doneMessage.classList.remove("visible");
    });
});

// Initial display
updateDisplay(remainingSeconds);
                        

🎯 Quick Quiz

Question 1: What does setTimeout return?

Question 2: What will this code output?


console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
                        

Question 3: What's the difference between setInterval and recursive setTimeout?

Question 4: Why use requestAnimationFrame instead of setInterval for visual animations?

Question 5: What is debouncing?

Summary

🎉 Key Takeaways

  • setTimeout(fn, ms) runs a function once after a delay; cancel with clearTimeout(id)
  • setInterval(fn, ms) runs a function repeatedly; cancel with clearInterval(id)
  • Recursive setTimeout is safer than setInterval for async work — it waits for completion before scheduling
  • Debounce waits for events to stop; Throttle limits frequency — both use setTimeout internally
  • Timer delays are minimums, not guarantees — the event loop processes callbacks only when the call stack is empty
  • Even setTimeout(fn, 0) waits for current code to finish (the call stack must be empty)
  • requestAnimationFrame syncs with the display for smooth animations and pauses in background tabs
  • Use the timestamp parameter from rAF for frame-rate-independent animation
  • CSS transitions are the best choice for simple state-change animations — JS just toggles a class
  • Use transitionend to run code after a CSS transition completes

📚 Additional Resources

🚀 What's Next?

Your pages can now respond to input and move on screen. But what happens when the user leaves? All that data disappears. In the next lesson, you'll learn to persist state with Local Storage — saving user preferences, form drafts, and app data so it survives page refreshes and browser restarts.