Build a Progress Bar with Controls
Interview Question: "Build a progress bar with start, pause, resume, stop, and restart functionality."
Requirements
- Visual progress bar from 0% to 100%
- Start/Resume, Pause, Stop buttons
- Restart resets to 0% and begins again
- Smooth animation
- Accessible (role="progressbar", aria-valuenow)
- Callback on completion
React Implementation
function useProgressBar({
duration = 3000,
onComplete,
}: {
duration?: number;
onComplete?: () => void;
}) {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<'idle' | 'running' | 'paused' | 'complete'>('idle');
const rafRef = useRef<number | null>(null);
const startTimeRef = useRef<number>(0);
const elapsedRef = useRef<number>(0);
const tick = useCallback((timestamp: number) => {
if (!startTimeRef.current) startTimeRef.current = timestamp;
const elapsed = elapsedRef.current + (timestamp - startTimeRef.current);
const pct = Math.min((elapsed / duration) * 100, 100);
setProgress(pct);
if (pct >= 100) {
setStatus('complete');
onComplete?.();
return;
}
rafRef.current = requestAnimationFrame(tick);
}, [duration, onComplete]);
const start = useCallback(() => {
if (status === 'running') return;
startTimeRef.current = 0;
setStatus('running');
rafRef.current = requestAnimationFrame(tick);
}, [status, tick]);
const pause = useCallback(() => {
if (status !== 'running') return;
if (rafRef.current) cancelAnimationFrame(rafRef.current);
elapsedRef.current += performance.now() - (startTimeRef.current || performance.now());
setStatus('paused');
}, [status]);
const resume = useCallback(() => {
if (status !== 'paused') return;
startTimeRef.current = 0;
setStatus('running');
rafRef.current = requestAnimationFrame(tick);
}, [status, tick]);
const stop = useCallback(() => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
setProgress(0);
elapsedRef.current = 0;
startTimeRef.current = 0;
setStatus('idle');
}, []);
const restart = useCallback(() => {
stop();
requestAnimationFrame(() => {
startTimeRef.current = 0;
setStatus('running');
rafRef.current = requestAnimationFrame(tick);
});
}, [stop, tick]);
useEffect(() => {
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, []);
return { progress, status, start, pause, resume, stop, restart };
}Component
function ProgressBar() {
const { progress, status, start, pause, resume, stop, restart } = useProgressBar({
duration: 5000,
onComplete: () => console.log('Done!'),
});
return (
<div className="progress-container">
<div
className="progress-track"
role="progressbar"
aria-valuenow={Math.round(progress)}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Task progress"
>
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
<span className="progress-label">{Math.round(progress)}%</span>
</div>
<div className="controls">
{status === 'idle' && <button onClick={start}>Start</button>}
{status === 'running' && <button onClick={pause}>Pause</button>}
{status === 'paused' && <button onClick={resume}>Resume</button>}
{status !== 'idle' && <button onClick={stop}>Stop</button>}
<button onClick={restart}>Restart</button>
</div>
<p aria-live="polite" className="sr-only">
Progress: {Math.round(progress)}%
{status === 'complete' && ' â Complete'}
</p>
</div>
);
}CSS
.progress-track {
width: 100%;
height: 24px;
background: #e5e7eb;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 12px;
transition: width 50ms linear;
}
.progress-label {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: #1f2937;
mix-blend-mode: difference;
}CSS-Only Alternative (Simple Version)
For a non-pausable progress bar, CSS animations are simpler:
.progress-fill {
width: 0%;
animation: fill 5s linear forwards;
}
@keyframes fill {
to { width: 100%; }
}
.progress-fill.paused {
animation-play-state: paused;
}requestAnimationFrame vs setInterval
| Approach | Precision | CPU Impact | Tab Background |
|---|---|---|---|
requestAnimationFrame | ~16.67ms (60fps) | Minimal â syncs with display | Throttled/paused |
setInterval | Can drift | Continues regardless | May continue |
requestAnimationFrame is the correct choice for visual animations because it aligns with the browser's paint cycle.
What Interviewers Look For
- Timer management â
requestAnimationFrameoversetInterval, proper cleanup withcancelAnimationFrame - State machine â Discrete states (idle/running/paused/complete) prevent invalid transitions
- Elapsed time tracking â Accumulating elapsed time on pause, not restarting from 0 on resume
- Accessibility â
role="progressbar",aria-valuenow,aria-livefor screen reader updates - Cleanup â Cancelling animation frame on unmount