Stopwatch

Stopwatch demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import { Component } from '@expressive/react';

class Stopwatch extends Component {
  elapsed = 0;
  running = false;

  // A single interval lives for the component's lifetime; it only
  // accumulates while `running`. Returning a cleanup clears it on unmount.
  protected new() {
    const id = setInterval(() => {
      if (this.running) this.elapsed += 10;
    }, 10);

    return () => clearInterval(id);
  }

  get display() {
    const cs = Math.floor(this.elapsed / 10) % 100;
    const s = Math.floor(this.elapsed / 1000) % 60;
    const m = Math.floor(this.elapsed / 60000);
    const pad = (n: number) => String(n).padStart(2, '0');

    return `${pad(m)}:${pad(s)}.${pad(cs)}`;
  }

  toggle() {
    this.running = !this.running;
  }

  reset() {
    this.running = false;
    this.elapsed = 0;
  }

  render() {
    const { display, running, elapsed, toggle, reset } = this;

    return (
      <div className="stopwatch">
        <h1>Stopwatch</h1>
        <p className={`time ${running ? 'running' : ''}`}>{display}</p>
        <div className="controls">
          <button onClick={toggle}>{running ? 'Stop' : 'Start'}</button>
          <button onClick={reset} disabled={!elapsed && !running}>
            Reset
          </button>
        </div>
      </div>
    );
  }
}

export default Stopwatch;

App.css

.stopwatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
}

.stopwatch h1 {
  margin: 0;
}

.time {
  margin: 0;
  font-size: var(--t-3xl, var(--t-2xl));
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05em;
  color: var(--fg-soft);
  transition: color 0.2s;
}

/* A subtle cue that time is advancing. */
.time.running {
  color: var(--accent);
}

.controls {
  display: flex;
  gap: var(--s2);
}