Fossils🛠ïļ Machine CodingBuild a Progress Bar with Controls
ðŸĢHatchlingMachine CodingAnimationTimer Management

Build a Progress Bar with Controls

A progress bar with start, pause, stop, and restart tests timer management, CSS transitions vs JS animation, and the requestAnimationFrame API.

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

ApproachPrecisionCPU ImpactTab Background
requestAnimationFrame~16.67ms (60fps)Minimal — syncs with displayThrottled/paused
setIntervalCan driftContinues regardlessMay continue

requestAnimationFrame is the correct choice for visual animations because it aligns with the browser's paint cycle.

What Interviewers Look For

  1. Timer management — requestAnimationFrame over setInterval, proper cleanup with cancelAnimationFrame
  2. State machine — Discrete states (idle/running/paused/complete) prevent invalid transitions
  3. Elapsed time tracking — Accumulating elapsed time on pause, not restarting from 0 on resume
  4. Accessibility — role="progressbar", aria-valuenow, aria-live for screen reader updates
  5. Cleanup — Cancelling animation frame on unmount