Why This Question Exists
Progress Bar I is a component-library design question. It looks trivial — a div with a width — but the interviewer is checking for CSS animation correctness, defensive prop handling, full ARIA accessibility, and whether you know when to use CSS transitions vs JS animation. Most candidates pass the visual test but fail the ARIA audit.
CSS Width Transition — not JS
/* ✅ CORRECT — GPU-accelerated, smooth, zero JS */
.fill {
width: var(--progress, 0%); /* or inline style */
transition: width 0.35s ease;
}
/* ❌ WRONG — JS animation loop, janky, burns CPU */
useEffect(() => {
let frame;
const animate = () => {
setDisplayWidth(w => w + 1);
if (displayWidth < target) frame = requestAnimationFrame(animate);
};
frame = requestAnimationFrame(animate);
return () => cancelAnimationFrame(frame);
}, [target]);
CSS transitions run on the compositor thread — they don't block JavaScript and produce silky-smooth 60fps animation even under load. JS-driven animation competes with React's reconciler for the main thread.
ℹ Interview TipSay: "I use CSS transition on the width property rather than a JavaScript animation loop. CSS transitions run on the GPU compositor thread independently of JavaScript, so the bar animates smoothly even if the main thread is busy processing a large React update."
Full ARIA Implementation
<div
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Upload progress" // OR aria-labelledby pointing to a label element
style={{ width: value + '%' }}
/>
Screen readers announce this as "Upload progress, 45 percent" automatically from these four attributes. Without them, the progress bar is completely invisible to users who rely on assistive technology — a WCAG 2.1 Level A failure.
Value Clamping — defensive component design
function ProgressBar({ value = 0 }) {
// Guard at the component boundary — protect against invalid props
const clamped = Math.min(100, Math.max(0, Number(value) || 0));
// Now clamped is always 0-100, even if caller passes:
// value={-10} → 0
// value={150} → 100
// value="50" → 50 (Number() coerces string)
// value={NaN} → 0 (|| 0 handles NaN)
// value={null} → 0
return <div style={{ width: clamped + '%' }} />;
}
Color and Label Props
function ProgressBar({ value, color = '#6366f1', label = 'Progress', showLabel = true }) {
const v = Math.min(100, Math.max(0, value));
return (
<div>
{showLabel && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{label}</span>
<span>{v}%</span>
</div>
)}
<div className="track">
<div role="progressbar" aria-valuenow={v} aria-valuemin={0} aria-valuemax={100}
style={{ width: v + '%', background: color, transition: 'width .35s' }} />
</div>
</div>
);
}
⚠ Common Pitfall: Animating transform instead of widthSome developers use transform: scaleX(value/100) thinking it's more performant. While transform is GPU-accelerated, scaleX scales from the centre by default — the fill shrinks/grows symmetrically. Use transform-origin: left to fix this, OR just use width — the performance difference is negligible for a simple progress bar.