Create a Stop Watch using React

Last Updated : 14 Oct, 2026

A stopwatch in React is a timer that measures elapsed time and lets users start, pause, resume, and reset the timer. It can be built using React state, Effects, event handlers, and reusable components.

  • useState stores the stopwatch status and elapsed time.
  • useEffect manages the interval and cleans it up when the stopwatch pauses or the component updates.

Create a React Application

Create a React project using Vite, then install its dependencies.

Step 1: Run the following command to create the project.

npm create vite@latest stopwatch -- --template react

Step 2: Move into the project directory.

cd stopwatch

Step 3: Install the dependencies.

npm install

Vite's current documentation lists Node.js 20.19+ or 22.12+ as supported requirements. Check the Vite guide if your environment reports a version error.

Set Up the Project Structure

Inside the src directory, create a components folder and add the following files.

stopwatch/
├── src/
│ ├── components/
│ │ ├── StopWatch.jsx
│ │ ├── Timer.jsx
│ │ └── ControlButtons.jsx
│ ├── App.jsx
│ ├── App.css
│ └── main.jsx
├── package.json
└── index.html

The StopWatch component manages the timer, Timer displays the elapsed time, and ControlButtons provides the stopwatch controls.

Configure the React Entry Point

Replace the contents of src/main.jsx with the following code. React's createRoot() API renders the application inside the root DOM element.

Components used in our applications are:

JavaScript
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './App.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Create the Main App Component

In src/App.jsx, import and render the StopWatch component.

JavaScript
import StopWatch from './components/StopWatch.jsx';

function App() {
  return (
    <main className="app">
      <h1>React Stopwatch</h1>
      <StopWatch />
    </main>
  );
}

export default App;

Implement the Stopwatch Logic

Create src/components/StopWatch.jsx. This component stores the elapsed time and running status. It uses performance.now() to measure elapsed time rather than assuming every interval callback runs exactly on schedule.

JavaScript
import { useEffect, useRef, useState } from 'react';
import Timer from './Timer.jsx';
import ControlButtons from './ControlButtons.jsx';

function StopWatch() {
  const [elapsed, setElapsed] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const [hasStarted, setHasStarted] = useState(false);

  const startTimeRef = useRef(0);
  const elapsedBeforeStartRef = useRef(0);

  useEffect(() => {
    if (!isRunning) return;

    const intervalId = setInterval(() => {
      setElapsed(
        elapsedBeforeStartRef.current +
          performance.now() - startTimeRef.current
      );
    }, 10);

    return () => clearInterval(intervalId);
  }, [isRunning]);

  const handleStart = () => {
    elapsedBeforeStartRef.current = 0;
    startTimeRef.current = performance.now();
    setElapsed(0);
    setHasStarted(true);
    setIsRunning(true);
  };

  const handlePauseResume = () => {
    if (isRunning) {
      const currentElapsed =
        elapsedBeforeStartRef.current +
        performance.now() - startTimeRef.current;

      elapsedBeforeStartRef.current = currentElapsed;
      setElapsed(currentElapsed);
      setIsRunning(false);
    } else {
      startTimeRef.current = performance.now();
      setIsRunning(true);
    }
  };

  const handleReset = () => {
    setIsRunning(false);
    setHasStarted(false);
    setElapsed(0);
    elapsedBeforeStartRef.current = 0;
    startTimeRef.current = 0;
  };

  return (
    <section className="stopwatch">
      <Timer time={elapsed} />
      <ControlButtons
        hasStarted={hasStarted}
        isRunning={isRunning}
        onStart={handleStart}
        onPauseResume={handlePauseResume}
        onReset={handleReset}
      />
    </section>
  );
}

export default StopWatch;

The component uses three state values and two refs:

  • elapsed stores the elapsed time in milliseconds.
  • isRunning indicates whether the stopwatch is running.
  • hasStarted determines whether to display the Start button or the other controls.
  • startTimeRef stores the timestamp when the current running period began.
  • elapsedBeforeStartRef stores the elapsed time accumulated before the current running period.

The Effect starts an interval only while the stopwatch is running and clears it during cleanup.

Display the Elapsed Time

Create src/components/Timer.jsx. This component converts the elapsed milliseconds into hours, minutes, seconds, and hundredths of a second.

JavaScript
function Timer({ time }) {
  const totalHundredths = Math.floor(time / 10);

  const hours = Math.floor(totalHundredths / 360000);
  const minutes = Math.floor(totalHundredths / 6000) % 60;
  const seconds = Math.floor(totalHundredths / 100) % 60;
  const hundredths = totalHundredths % 100;

  const format = (value) => String(value).padStart(2, '0');

  return (
    <div className="timer" role="timer" aria-label="Elapsed time">
      {format(hours)}:{format(minutes)}:{format(seconds)}.
      {format(hundredths)}
    </div>
  );
}

export default Timer;

The display uses the HH:MM:SS.hh format. The final two digits represent hundredths of a second, not individual milliseconds.

Add the Stopwatch Controls

Create src/components/ControlButtons.jsx. These buttons call the event handlers received from the parent component.

JavaScript
function ControlButtons({
  hasStarted,
  isRunning,
  onStart,
  onPauseResume,
  onReset
}) {
  return (
    <div className="controls">
      {!hasStarted ? (
        <button type="button" onClick={onStart}>
          Start
        </button>
      ) : (
        <>
          <button type="button" onClick={onReset}>
            Reset
          </button>

          <button type="button" onClick={onPauseResume}>
            {isRunning ? 'Pause' : 'Resume'}
          </button>
        </>
      )}
    </div>
  );
}

export default ControlButtons;

The controls change according to the stopwatch state:

  • Before starting, only the Start button is displayed.
  • While running, Reset and Pause are displayed.
  • While paused, Reset and Resume are displayed.

Style the Stopwatch

Add the following CSS to src/App.css to style the application and its controls.

CSS
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background: #eeeeee;
}

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.app h1 {
  color: #0d0c1b;
}

.stopwatch {
  width: min(100%, 420px);
  padding: 32px 20px;
  border-radius: 12px;
  background: #0d0c1b;
  text-align: center;
}

.timer {
  margin-bottom: 32px;
  color: #f5f5f5;
  font-family: monospace;
  font-size: clamp(1.8rem, 7vw, 3rem);
  font-variant-numeric: tabular-nums;
}

.controls {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.controls button {
  min-width: 100px;
  padding: 12px 16px;
  border: 0;
  border-radius: 8px;
  background: #e42a2a;
  color: white;
  font-size: 1rem;
  cursor: pointer;
}

.controls button:hover {
  background: #bd2020;
}

.controls button:focus-visible {
  outline: 3px solid white;
  outline-offset: 3px;
}

Run the Application

From the project root directory, start the Vite development server.

npm run dev

Open the local URL shown in the terminal.

Final Output:

Test the Stopwatch

Verify the following behaviors in the browser:

  • Click Start and confirm that the elapsed time increases.
  • Click Pause and confirm that the displayed time stops increasing.
  • Click Resume and confirm that timing continues from the paused value.
  • Click Reset and confirm that the display returns to 00:00:00.00 and only Start is shown.
Comment