components

Building a Reusable Progress Bar

Published: September 8, 2026

⏱️ 2 min read | 📝 393 words

Player Health
72/100
Loading Assets
80/100

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.

The payoff

<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 percentageMath.min(100, Math.max(0, ...)) means a value above max or below 0 can never break the layout or overflow the track.
  • Real progress semanticsrole="progressbar" plus aria-valuenow and aria-valuemax make 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: width keeps 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.

Where to use it

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.