Skip to main content

Overview

The Pomodoro Timer is a full-featured productivity app demonstrating:
  • Functional components with hooks
  • useState and useEffect hooks
  • Timer implementation with intervals
  • Browser notifications API
  • Theme switching
  • Settings management

Live Demo

View the complete source code on GitHub

What You’ll Learn

Hooks

Using useState and useEffect

Side Effects

Managing timers and intervals

Browser APIs

Working with notifications

Complex State

Managing multiple related states

Complete Code

Timer Component with Hooks

timer.js
import {
  h,
  createComponent,
  useState,
  useEffect,
  createApp,
} from "@glyphui/runtime";

const formatTime = (seconds) => {
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${mins.toString().padStart(2, "0")}:${secs
    .toString()
    .padStart(2, "0")}`;
};

const Timer = () => {
  // State
  const [workTime, setWorkTime] = useState(25 * 60);
  const [breakTime, setBreakTime] = useState(5 * 60);
  const [timeLeft, setTimeLeft] = useState(workTime);
  const [isActive, setIsActive] = useState(false);
  const [isBreak, setIsBreak] = useState(false);
  const [workSessions, setWorkSessions] = useState(0);
  const [breakSessions, setBreakSessions] = useState(0);
  const [showSettings, setShowSettings] = useState(false);
  const [notificationsEnabled, setNotificationsEnabled] = useState(false);
  const [theme, setTheme] = useState("light");
  const [workMinutes, setWorkMinutes] = useState(25);
  const [breakMinutes, setBreakMinutes] = useState(5);

  const totalTime = isBreak ? breakTime : workTime;
  const progress = ((totalTime - timeLeft) / totalTime) * 100;

  // Theme effect
  useEffect(() => {
    document.body.setAttribute("data-theme", theme);
  }, [theme]);

  // Timer effect
  useEffect(() => {
    let interval = null;

    if (isActive && timeLeft > 0) {
      interval = setInterval(() => {
        setTimeLeft(timeLeft - 1);
      }, 1000);
    } else if (isActive && timeLeft === 0) {
      if (isBreak) {
        setBreakSessions(breakSessions + 1);
        setIsBreak(false);
        setTimeLeft(workTime);
        if (notificationsEnabled) {
          notify("Break finished", "Time to focus!");
        }
      } else {
        setWorkSessions(workSessions + 1);
        setIsBreak(true);
        setTimeLeft(breakTime);
        if (notificationsEnabled) {
          notify("Work session completed", "Time for a break!");
        }
      }
    }

    return () => clearInterval(interval);
  }, [isActive, timeLeft, isBreak, workTime, breakTime]);

  const toggleTimer = () => {
    setIsActive(!isActive);
  };

  const resetTimer = () => {
    setIsActive(false);
    setTimeLeft(isBreak ? breakTime : workTime);
  };

  const applySettings = (e) => {
    e.preventDefault();
    const newWorkTime = workMinutes * 60;
    const newBreakTime = breakMinutes * 60;
    setWorkTime(newWorkTime);
    setBreakTime(newBreakTime);
    if (!isActive) {
      setTimeLeft(isBreak ? newBreakTime : newWorkTime);
    }
    setShowSettings(false);
  };

  const toggleNotifications = () => {
    if (!notificationsEnabled && "Notification" in window) {
      Notification.requestPermission().then((permission) => {
        if (permission === "granted") {
          setNotificationsEnabled(true);
        }
      });
    } else {
      setNotificationsEnabled(!notificationsEnabled);
    }
  };

  const notify = (title, body) => {
    if ("Notification" in window && Notification.permission === "granted") {
      new Notification(title, { body });
    }
  };

  const toggleTheme = () => {
    setTheme(theme === "light" ? "dark" : "light");
  };

  return h("div", {}, [
    // Timer display
    h("div", { class: `timer-card ${isBreak ? "break" : ""}` }, [
      h("div", { class: "timer-label" }, [
        isBreak ? "Break Time" : "Focus Time",
      ]),
      h("div", { class: `timer-display ${isBreak ? "break" : ""}` }, [
        formatTime(timeLeft),
      ]),
      h(
        "div",
        {
          class: `timer-progress ${isBreak ? "break" : ""}`,
          style: { width: `${progress}%` },
        },
        []
      ),
    ]),

    // Controls
    h("div", { class: "controls" }, [
      h(
        "button",
        {
          class: "btn-primary",
          on: { click: toggleTimer },
        },
        [
          isActive ? createComponent(PauseIcon) : createComponent(PlayIcon),
          isActive ? "Pause" : "Start",
        ]
      ),
      h(
        "button",
        {
          class: "btn-secondary",
          on: { click: resetTimer },
        },
        [createComponent(ResetIcon), "Reset"]
      ),
      h(
        "button",
        {
          class: "btn-secondary",
          on: { click: () => setShowSettings(!showSettings) },
        },
        ["Settings"]
      ),
      h(
        "button",
        {
          class: "theme-toggle",
          on: { click: toggleTheme },
        },
        [
          theme === "light"
            ? createComponent(MoonIcon)
            : createComponent(SunIcon),
        ]
      ),
    ]),

    // Settings panel
    showSettings &&
      h("div", { class: "settings-card" }, [
        h("div", { class: "settings-title" }, ["Timer Settings"]),
        h(
          "form",
          {
            class: "settings-form",
            on: { submit: applySettings },
          },
          [
            h("div", { class: "form-group" }, [
              h("label", { for: "workTime" }, ["Work Minutes"]),
              h(
                "input",
                {
                  id: "workTime",
                  type: "number",
                  min: "1",
                  max: "60",
                  value: workMinutes,
                  on: {
                    input: (e) =>
                      setWorkMinutes(parseInt(e.target.value) || 25),
                  },
                },
                []
              ),
            ]),
            h("div", { class: "form-group" }, [
              h("label", { for: "breakTime" }, ["Break Minutes"]),
              h(
                "input",
                {
                  id: "breakTime",
                  type: "number",
                  min: "1",
                  max: "30",
                  value: breakMinutes,
                  on: {
                    input: (e) =>
                      setBreakMinutes(parseInt(e.target.value) || 5),
                  },
                },
                []
              ),
            ]),
            h(
              "button",
              {
                class: "btn-primary",
                type: "submit",
              },
              ["Apply Settings"]
            ),
          ]
        ),
        h("div", { class: "notification-toggle" }, [
          h("label", { class: "toggle-switch" }, [
            h(
              "input",
              {
                type: "checkbox",
                checked: notificationsEnabled,
                on: { change: toggleNotifications },
              },
              []
            ),
            h("span", { class: "toggle-slider" }, []),
          ]),
          "Enable Notifications",
        ]),
      ]),

    // Session stats
    h("div", { class: "sessions-card" }, [
      h("h3", { class: "sessions-title" }, ["Session Stats"]),
      h("div", { class: "sessions-counter" }, [
        h("div", { class: "counter-item" }, [
          h("div", { class: "counter-value" }, [workSessions]),
          h("div", { class: "counter-label" }, ["Work Sessions"]),
        ]),
        h("div", { class: "counter-item" }, [
          h("div", { class: "counter-value break" }, [breakSessions]),
          h("div", { class: "counter-label" }, ["Break Sessions"]),
        ]),
      ]),
    ]),
  ]);
};

const app = createApp({
  view: () => createComponent(Timer),
});

app.mount(document.getElementById("app"));

Key Concepts

1. useState Hook

Manage component state functionally:
const [timeLeft, setTimeLeft] = useState(25 * 60);
const [isActive, setIsActive] = useState(false);
Unlike class components, hooks let you split state into multiple independent pieces.

2. useEffect Hook

Handle side effects like timers:
useEffect(() => {
  let interval = null;
  
  if (isActive && timeLeft > 0) {
    interval = setInterval(() => {
      setTimeLeft(timeLeft - 1);
    }, 1000);
  }
  
  return () => clearInterval(interval); // Cleanup
}, [isActive, timeLeft]); // Dependencies
The effect runs when dependencies change, and the cleanup function runs before the next effect.

3. Browser Notifications

Request permission and show notifications:
const toggleNotifications = () => {
  if ("Notification" in window) {
    Notification.requestPermission().then((permission) => {
      if (permission === "granted") {
        setNotificationsEnabled(true);
      }
    });
  }
};

const notify = (title, body) => {
  if (Notification.permission === "granted") {
    new Notification(title, { body });
  }
};

4. Progress Calculation

Calculate progress as a percentage:
const totalTime = isBreak ? breakTime : workTime;
const progress = ((totalTime - timeLeft) / totalTime) * 100;

Features Demonstrated

  • Work/break cycle with automatic switching
  • Start, pause, and reset controls
  • Configurable work and break durations
  • Progress bar visualization
  • Multiple useState hooks for different concerns
  • Derived state (progress calculation)
  • Persistent settings within session
  • setInterval for countdown timer
  • Proper cleanup to prevent memory leaks
  • Effect dependencies for optimal re-runs
  • DOM manipulation for theme
  • Visual feedback with colors
  • Browser notifications at session end
  • Theme switching (light/dark)
  • Session statistics tracking

Running the Example

1

Clone the repository

git clone https://github.com/x0bd/glyphui.git
cd glyphui/examples/pomodoro-timer
2

Open in browser

Open index.html in your browser:
npx serve .
3

Start a session

  • Click Start to begin a work session
  • Watch the timer count down
  • Wait for automatic break transition
  • Try adjusting settings
4

Enable notifications

  • Toggle the notifications setting
  • Grant permission when prompted
  • Complete a session to see notification

Best Practices

Always clean up intervals and timeouts in useEffect cleanup functions to prevent memory leaks:
return () => clearInterval(interval);
Use dependency arrays carefully. Include all values from the component scope that the effect uses:
useEffect(() => {
  // uses isActive and timeLeft
}, [isActive, timeLeft]);

Next Steps

Hooks Guide

Learn all about hooks in GlyphUI

Tic-Tac-Toe

See game logic implementation

useEffect API

Detailed useEffect documentation

useState API

Detailed useState documentation

Build docs developers (and LLMs) love