Building a Reusable Progress Bar
Published: September 8, 2026
⏱️ 2 min read | 📝 393 words
Why a progress bar?
Progress bars show up everywhere: loading screens, upload and download managers, multi-step forms, and in games as health bars, stamina meters, and XP tracks. The moment you need one in a second project, you realize how nice it would be to have a single, reusable component you can just drop in.
That's exactly what this post covers — a small Astro component that works in both web and game contexts.
<ProgressBar value={72} max={100} label="Player Health" /> is all you need to render an accessible, styled progress bar.
The component
---
interface Props {
label?: string;
value: number;
max?: number;
showValue?: boolean;
}
const { label, value, max = 100, showValue = true } = Astro.props;
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
---
<div class="progress-bar-wrapper">
{label && <span class="progress-bar-label">{label}</span>}
<div class="progress-bar" role="progressbar" aria-valuenow={value} aria-valuemax={max}>
<div class="progress-bar-fill" style={`width: ${percentage}%`}></div>
</div>
{showValue && <span class="progress-bar-value">{value}/{max}</span>}
</div>
<style>
.progress-bar-wrapper {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.progress-bar {
flex: 1;
height: 12px;
background: #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #16a34a);
border-radius: inherit;
transition: width 0.3s ease;
}
.progress-bar-value {
font-size: 0.9rem;
white-space: nowrap;
}
</style>Key decisions
- Clamped percentage —
Math.min(100, Math.max(0, ...))means a value abovemaxor below0can never break the layout or overflow the track. - Real progress semantics —
role="progressbar"plusaria-valuenowandaria-valuemaxmake the current state readable to screen readers. - Scaled width, not color math — the fill is a simple width percentage, so you can swap in any background gradient without touching the logic.
- Smooth updates — a 300ms
transition: widthkeeps value changes from feeling jumpy when the fill moves.
Customizing it
The two things people usually tweak are the height of the track and the fill gradient:
.progress-bar {
height: 20px; /* chunkier, more "game UI" */
}
.progress-bar-fill {
background: linear-gradient(90deg, #f59e0b, #ef4444); /* stamina -> danger */
}If you end up with several themes, give the component a variant prop later and map it to a set of classes — same logic, different look.
It's part of the UI Elements Playground project — a shared home for components used across both web apps and game UIs. Check the project page for the full picture.