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 reactStep 2: Move into the project directory.
cd stopwatchStep 3: Install the dependencies.
npm installVite'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:
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.
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.
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.
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.
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.
* {
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 devOpen 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.