12 examples Responsive Uses JS beginner

12 CSS Progress Bar Designs

A CSS progress bar is a visual indicator of how far a task or value has progressed toward a target — file uploads, multi-step forms, skill levels, fundraising goals, video buffer state, storage usage and more. These 12 hand-coded designs cover every common shape: linear bars, circular rings, conic gauges, segmented batteries, vertical goal meters and split stacked categories. Every demo uses semantic markup (real progress, fieldset, or labelled svg), modern CSS (@property animatable angles, conic gradients, stroke-dasharray with pathLength=100), and JavaScript only where a counter genuinely needs to stay in sync with a CSS transition.

Twelve premium CSS progress bar designs covering every real pattern — linear, circular, segmented, stacked, gauge, stepped, vertical and buffered. Built with semantic HTML, modern standards-based CSS (@property, :has(), conic gradients, prefers-reduced-motion), proper role="progressbar" and aria-valuenow attributes for accessibility, and JavaScript only where it adds real interaction. Each demo uses a different accent palette so you see the brand range at a glance — drop any of them straight into your project.

12 unique progress bars WCAG-friendly accessible Mobile-first responsive MIT free to use
01 / 12
Liquid Fill
Pure CSS
68% storage used
Wave-animated water rising inside a rounded vessel — perfect for resource meters, storage usage, and "fill your goal" patterns. Two SVG waves drift at different speeds for organic motion.
.pb-liquid {
  position: relative;
  width: 220px; height: 88px;
  border-radius: 14px;
  background: linear-gradient(180deg, #0f172a, #1e293b);
  border: 1px solid rgba(56,189,248,0.25);
  overflow: hidden;
  box-shadow: 0 12px 30px -10px rgba(56,189,248,0.25);
  font-family: system-ui, sans-serif;
}
.pb-liquid-fill {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--pb-liquid-level, 0%);
  background: linear-gradient(180deg, rgba(56,189,248,0.85) 0%, rgba(14,165,233,0.95) 100%);
  color: rgba(56,189,248,0.85); /* feeds the SVG waves */
  transition: height 0.6s cubic-bezier(.5,0,.3,1.2);
}
.pb-liquid-wave {
  position: absolute; left: 0; bottom: 100%;
  width: 200%; height: 18px;
}
.pb-liquid-wave.w1 { animation: pbLiquidWave 4s linear infinite; opacity: 0.85; }
.pb-liquid-wave.w2 { animation: pbLiquidWave 6s linear infinite reverse; opacity: 0.55; }
@keyframes pbLiquidWave {
  to { transform: translateX(-50%); }
}
.pb-liquid-label {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: #f0f9ff;
  font-size: 14px;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 4px rgba(0,0,0,0.45);
  z-index: 1;
}
.pb-liquid-label strong { font-size: 20px; font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .pb-liquid-wave { animation: none; }
}
<div class="pb-liquid" role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Storage used">
  <span class="pb-liquid-fill" style="--pb-liquid-level: 68%;">
    <svg class="pb-liquid-wave w1" viewBox="0 0 200 20" preserveAspectRatio="none" aria-hidden="true">
      <path d="M0 10 Q 25 0 50 10 T 100 10 T 150 10 T 200 10 V 20 H 0 Z" fill="currentColor"/>
    </svg>
    <svg class="pb-liquid-wave w2" viewBox="0 0 200 20" preserveAspectRatio="none" aria-hidden="true">
      <path d="M0 10 Q 25 20 50 10 T 100 10 T 150 10 T 200 10 V 20 H 0 Z" fill="currentColor"/>
    </svg>
  </span>
  <span class="pb-liquid-label"><strong>68%</strong> storage used</span>
</div>
02 / 12
Step Tracker
Pure CSS
  1. 1Cart
  2. 2Address
  3. 3Payment
  4. 4Confirm
Numbered steps with a connecting rail that fills as you progress. Completed steps morph into checkmarks via pseudo-elements — ideal for checkout flows, onboarding wizards, and multi-page forms.
.pb-step {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
  width: 280px;
  list-style: none;
  margin: 0; padding: 0;
  font-family: system-ui, sans-serif;
  isolation: isolate;
}
.pb-step::before,
.pb-step::after {
  content: ''; position: absolute;
  left: 12.5%; right: 12.5%;
  top: 15px; height: 2px;
  border-radius: 2px;
  z-index: 0;
}
.pb-step::before { background: rgba(255,255,255,0.08); }
.pb-step::after  {
  background: linear-gradient(90deg, #14b8a6, #2dd4bf);
  width: 0; left: 12.5%; right: auto;
  transition: width 0.5s cubic-bezier(.5,0,.3,1.2);
}
.pb-step[data-current="2"]::after { width: calc(((2 - 1) / (4 - 1)) * 75%); }
.pb-step[data-current="3"]::after { width: calc(((3 - 1) / (4 - 1)) * 75%); }
.pb-step[data-current="4"]::after { width: 75%; }
.pb-step-item {
  position: relative;
  display: grid; gap: 6px; place-items: center;
  font-size: 11px; color: #94a3b8;
  z-index: 1;
}
.pb-step-item span {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border-radius: 50%;
  background: #1e293b;
  border: 2px solid rgba(255,255,255,0.08);
  color: #94a3b8;
  font-weight: 700; font-size: 13px;
  transition: background 0.3s, border-color 0.3s, color 0.3s;
}
.pb-step-item em {
  font-style: normal; font-size: 10.5px;
  letter-spacing: 0.06em; text-transform: uppercase;
  font-weight: 600;
}
.pb-step-item.is-done span,
.pb-step-item.is-current span {
  background: linear-gradient(135deg, #14b8a6, #2dd4bf);
  border-color: #2dd4bf;
  color: #0f172a;
  box-shadow: 0 0 0 4px rgba(20,184,166,0.15);
}
.pb-step-item.is-done span { font-size: 0; }
.pb-step-item.is-done span::before {
  content: '';
  width: 13px; height: 7px;
  border: 2px solid #0f172a;
  border-top: 0; border-right: 0;
  transform: rotate(-45deg);
  transform-origin: center;
}
.pb-step-item.is-current em { color: #2dd4bf; }
.pb-step-item.is-done em    { color: #cbd5e1; }
<ol class="pb-step" role="list" data-current="3">
  <li class="pb-step-item is-done"><span>1</span><em>Cart</em></li>
  <li class="pb-step-item is-done"><span>2</span><em>Address</em></li>
  <li class="pb-step-item is-current"><span>3</span><em>Payment</em></li>
  <li class="pb-step-item"><span>4</span><em>Confirm</em></li>
</ol>
03 / 12
Conic Ring
Pure CSS
74% Profile
A circular progress ring rendered with a real `@property`-animated conic gradient — no SVG, no JS scoring. The percentage in the centre updates with the value via a CSS custom property, and the fill sweeps from 0 to the target on every render.
@property --pb-ring-deg {
  syntax: '<angle>'; inherits: false; initial-value: 0deg;
}
.pb-ring {
  position: relative;
  width: 120px; height: 120px;
  border-radius: 50%;
  --pb-ring-deg: calc(var(--pb-ring-value, 0) * 3.6deg);
  background:
    conic-gradient(
      #c084fc 0deg,
      #f472b6 var(--pb-ring-deg),
      rgba(255,255,255,0.06) var(--pb-ring-deg),
      rgba(255,255,255,0.06) 360deg
    );
  animation: pbRingDraw 1.4s cubic-bezier(.5,0,.3,1.2) forwards;
  font-family: system-ui, sans-serif;
}
@keyframes pbRingDraw {
  from { --pb-ring-deg: 0deg; }
  to   { --pb-ring-deg: calc(var(--pb-ring-value, 0) * 3.6deg); }
}
.pb-ring-track {
  position: absolute; inset: 8px;
  border-radius: 50%;
  background: #15151d;
  box-shadow: inset 0 0 18px rgba(192,132,252,0.15);
}
.pb-ring-meta {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 4px;
  line-height: 1;
}
.pb-ring-meta strong {
  display: block;
  font-size: 26px; font-weight: 700;
  color: #f0eeff;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
}
.pb-ring-meta strong small {
  font-size: 13px; font-weight: 600;
  color: #c084fc; margin-left: 1px;
}
.pb-ring-meta em {
  display: block;
  font-style: normal;
  font-size: 9.5px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: #a78bfa;
  line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
  .pb-ring { animation: none; }
}
<div class="pb-ring" role="progressbar" aria-valuenow="74" aria-valuemin="0" aria-valuemax="100" aria-label="Profile completion" style="--pb-ring-value: 74;">
  <span class="pb-ring-track" aria-hidden="true"></span>
  <span class="pb-ring-meta">
    <strong>74<small>%</small></strong>
    <em>Profile</em>
  </span>
</div>
04 / 12
Segmented Battery
Pure CSS
Signal 3 / 5 bars
Discrete signal-bar segments that fill from left to right. Each segment has a distinct highlight, giving a tactile, hardware-keypad feel — great for upload progress and connection strength.
.pb-seg {
  width: 220px;
  display: grid; gap: 6px;
  font-family: system-ui, sans-serif;
}
.pb-seg-label {
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: #94a3b8;
}
.pb-seg-rail {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 4px;
}
.pb-seg-cell {
  height: 22px;
  border-radius: 4px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.06);
  position: relative;
  overflow: hidden;
  transition: background 0.4s ease;
}
.pb-seg[data-pb-seg="1"] .pb-seg-cell:nth-child(-n+1),
.pb-seg[data-pb-seg="2"] .pb-seg-cell:nth-child(-n+2),
.pb-seg[data-pb-seg="3"] .pb-seg-cell:nth-child(-n+3),
.pb-seg[data-pb-seg="4"] .pb-seg-cell:nth-child(-n+4),
.pb-seg[data-pb-seg="5"] .pb-seg-cell:nth-child(-n+5) {
  background: linear-gradient(180deg, #34d399 0%, #10b981 100%);
  border-color: #34d399;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.3);
}
.pb-seg[data-pb-seg="1"] .pb-seg-cell:nth-child(-n+1)::after,
.pb-seg[data-pb-seg="2"] .pb-seg-cell:nth-child(-n+2)::after,
.pb-seg[data-pb-seg="3"] .pb-seg-cell:nth-child(-n+3)::after,
.pb-seg[data-pb-seg="4"] .pb-seg-cell:nth-child(-n+4)::after,
.pb-seg[data-pb-seg="5"] .pb-seg-cell:nth-child(-n+5)::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 0; height: 40%;
  background: linear-gradient(180deg, rgba(255,255,255,0.25), transparent);
}
.pb-seg-value {
  font-size: 12px; color: #6ee7b7;
  font-family: 'JetBrains Mono', monospace;
  letter-spacing: 0.04em;
}
<div class="pb-seg" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Network signal" data-pb-seg="3" data-pb-seg-total="5">
  <span class="pb-seg-label">Signal</span>
  <span class="pb-seg-rail" aria-hidden="true">
    <span class="pb-seg-cell"></span>
    <span class="pb-seg-cell"></span>
    <span class="pb-seg-cell"></span>
    <span class="pb-seg-cell"></span>
    <span class="pb-seg-cell"></span>
  </span>
  <span class="pb-seg-value">3 / 5 bars</span>
</div>
05 / 12
Stripe Loading
Pure CSS
Build #482 62%
Compiling assets · 4.2 MB / 6.8 MB
Diagonal candy-stripes shimmer continuously inside the filled portion — instantly reads as "actively in progress." A premium touch lifted from server-software install dialogs and CI build runners.
.pb-stripe {
  width: 240px;
  display: grid; gap: 6px;
  font-family: system-ui, sans-serif;
}
.pb-stripe-head {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 12px; font-weight: 600;
  color: #fde68a;
  font-family: 'JetBrains Mono', monospace;
  letter-spacing: 0.04em;
}
.pb-stripe-pct {
  background: rgba(245,158,11,0.15);
  color: #fbbf24;
  padding: 2px 8px; border-radius: 4px;
  font-variant-numeric: tabular-nums;
}
.pb-stripe-rail {
  height: 14px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(245,158,11,0.18);
  border-radius: 7px;
  overflow: hidden;
}
.pb-stripe-fill {
  width: var(--pb-stripe-w, 0%);
  height: 100%;
  background:
    repeating-linear-gradient(
      -45deg,
      #f59e0b 0,
      #f59e0b 8px,
      #fbbf24 8px,
      #fbbf24 16px
    );
  background-size: 22px 22px;
  animation: pbStripeMove 0.8s linear infinite;
  transition: width 0.5s cubic-bezier(.5,0,.3,1.2);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.15);
}
@keyframes pbStripeMove {
  from { background-position: 0 0; }
  to   { background-position: 44px 0; }
}
.pb-stripe-foot {
  font-size: 11px; color: #94a3b8;
  letter-spacing: 0.02em;
}
@media (prefers-reduced-motion: reduce) {
  .pb-stripe-fill { animation: none; }
}
<div class="pb-stripe" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="Build progress">
  <div class="pb-stripe-head">
    <span>Build #482</span>
    <span class="pb-stripe-pct">62%</span>
  </div>
  <div class="pb-stripe-rail">
    <div class="pb-stripe-fill" style="--pb-stripe-w: 62%;"></div>
  </div>
  <div class="pb-stripe-foot">Compiling assets · 4.2 MB / 6.8 MB</div>
</div>
06 / 12
Gradient Pulse
Pure CSS
Syncing your workspace 48%
A premium hero progress bar with a glowing leading edge that pulses gently. The trailing gradient shimmer creates a "comet" feel — perfect for app launchers, splash screens, and SaaS dashboards.
.pb-pulse {
  width: 260px;
  display: grid; gap: 8px;
  font-family: system-ui, sans-serif;
}
.pb-pulse-head {
  display: flex; justify-content: space-between; align-items: baseline;
  color: #f0eeff;
  font-size: 13px;
}
.pb-pulse-head strong { font-weight: 600; }
.pb-pulse-pct {
  font-family: 'JetBrains Mono', monospace;
  color: #c4b5fd;
  font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.pb-pulse-rail {
  height: 6px;
  background: rgba(255,255,255,0.06);
  border-radius: 99px;
  overflow: hidden;
  position: relative;
}
.pb-pulse-fill {
  position: relative;
  width: var(--pb-pulse-w, 0%);
  height: 100%;
  background: linear-gradient(90deg,
    rgba(124,108,255,0.25) 0%,
    #7c6cff 60%,
    #c4b5fd 100%);
  border-radius: inherit;
  transition: width 0.5s cubic-bezier(.5,0,.3,1.2);
}
.pb-pulse-edge {
  position: absolute; right: -4px; top: -2px; bottom: -2px;
  width: 10px;
  background: radial-gradient(circle, #fff 0%, rgba(196,181,253,0.55) 60%, transparent 80%);
  border-radius: 50%;
  animation: pbPulseGlow 1.4s ease-in-out infinite;
}
@keyframes pbPulseGlow {
  0%, 100% { opacity: 0.6; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.25); }
}
@media (prefers-reduced-motion: reduce) {
  .pb-pulse-edge { animation: none; }
}
<div class="pb-pulse" role="progressbar" aria-valuenow="48" aria-valuemin="0" aria-valuemax="100" aria-label="Sync progress">
  <div class="pb-pulse-head">
    <strong>Syncing your workspace</strong>
    <span class="pb-pulse-pct">48%</span>
  </div>
  <div class="pb-pulse-rail">
    <div class="pb-pulse-fill" style="--pb-pulse-w: 48%;">
      <span class="pb-pulse-edge" aria-hidden="true"></span>
    </div>
  </div>
</div>
07 / 12
Goal Tracker
Pure CSS
$6,800 raised of $10,000
Vertical fundraising-style progress with milestone markers. Each tier ($1k, $5k, $10k) lights up as the bar grows past it — ideal for crowdfunding, sales targets, and quarterly goals.
.pb-goal {
  width: 200px; height: 200px;
  display: grid;
  grid-template-columns: 50px 1fr;
  gap: 16px;
  font-family: system-ui, sans-serif;
}
.pb-goal-track {
  position: relative;
  width: 12px; height: 100%;
  margin-left: 18px;
  background: rgba(255,255,255,0.06);
  border-radius: 6px;
  overflow: visible;
}
.pb-goal-fill {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--pb-goal-pct, 0%);
  background: linear-gradient(180deg, #ec4899, #f43f5e);
  border-radius: 6px;
  box-shadow: 0 0 18px rgba(236,72,153,0.5);
  transition: height 0.7s cubic-bezier(.5,0,.3,1.2);
}
.pb-goal-marks {
  position: absolute; inset: 0;
  list-style: none; margin: 0; padding: 0;
}
.pb-goal-marks li {
  position: absolute; right: -28px;
  bottom: var(--m);
  transform: translateY(50%);
  font-size: 10px; font-weight: 700;
  color: #475569;
  letter-spacing: 0.04em;
  font-family: 'JetBrains Mono', monospace;
  display: flex; align-items: center; gap: 6px;
  transition: color 0.3s;
}
.pb-goal-marks li::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #1e293b;
  border: 2px solid #475569;
  margin-left: -28px;
  transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
}
.pb-goal-marks li.hit { color: #fbcfe8; }
.pb-goal-marks li.hit::before {
  background: #ec4899;
  border-color: #f9a8d4;
  box-shadow: 0 0 10px rgba(236,72,153,0.7);
}
.pb-goal-meta {
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 2px;
  color: #f0eeff;
}
.pb-goal-meta strong {
  font-size: 22px; font-weight: 700;
  color: #fda4af;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.pb-goal-meta span { font-size: 11px; color: #94a3b8; }
.pb-goal-meta em { font-style: normal; color: #fda4af; font-weight: 600; }
<div class="pb-goal" role="progressbar" aria-valuenow="6800" aria-valuemin="0" aria-valuemax="10000" aria-label="Funds raised" style="--pb-goal-pct: 68%;">
  <div class="pb-goal-track">
    <div class="pb-goal-fill"></div>
    <ul class="pb-goal-marks" aria-hidden="true">
      <li style="--m: 100%;"><span>$10k</span></li>
      <li style="--m: 50%;" class="hit"><span>$5k</span></li>
      <li style="--m: 10%;" class="hit"><span>$1k</span></li>
    </ul>
  </div>
  <div class="pb-goal-meta">
    <strong>$6,800</strong>
    <span>raised of <em>$10,000</em></span>
  </div>
</div>
08 / 12
Skill Bars
Pure CSS
TypeScript
92%
React
88%
CSS & Sass
95%
Figma
78%
Stacked horizontal bars with labels and percentages. Each row slides in with a staggered animation — the canonical pattern for portfolios, CV pages, and team skill matrices.
.pb-skill {
  width: 260px;
  margin: 0; padding: 0;
  display: grid; gap: 10px;
  font-family: system-ui, sans-serif;
}
.pb-skill-row {
  display: grid;
  grid-template-columns: 90px 1fr;
  align-items: center;
  gap: 10px;
}
.pb-skill dt {
  font-size: 11.5px; font-weight: 600;
  color: #fef3c7;
  letter-spacing: 0.02em;
}
.pb-skill dd {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 8px;
}
.pb-skill-rail {
  display: block;
  height: 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 99px;
  overflow: hidden;
}
.pb-skill-fill {
  display: block;
  height: 100%; width: 0;
  background: linear-gradient(90deg, #f97316, #fbbf24);
  border-radius: inherit;
  animation: pbSkillSlide 1s cubic-bezier(.5,0,.3,1.2) var(--pb-skill-delay, 0s) forwards;
}
@keyframes pbSkillSlide {
  to { width: var(--pb-skill-w, 0%); }
}
.pb-skill-pct {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; font-weight: 700;
  color: #fbbf24;
  font-variant-numeric: tabular-nums;
  min-width: 30px; text-align: right;
}
@media (prefers-reduced-motion: reduce) {
  .pb-skill-fill { animation: none; width: var(--pb-skill-w, 0%); }
}
<dl class="pb-skill">
  <div class="pb-skill-row">
    <dt>TypeScript</dt>
    <dd>
      <span class="pb-skill-rail">
        <span class="pb-skill-fill" style="--pb-skill-w: 92%;"></span>
      </span>
      <span class="pb-skill-pct">92%</span>
    </dd>
  </div>
  <div class="pb-skill-row">
    <dt>React</dt>
    <dd>
      <span class="pb-skill-rail">
        <span class="pb-skill-fill" style="--pb-skill-w: 88%; --pb-skill-delay: .1s;"></span>
      </span>
      <span class="pb-skill-pct">88%</span>
    </dd>
  </div>
  <div class="pb-skill-row">
    <dt>CSS &amp; Sass</dt>
    <dd>
      <span class="pb-skill-rail">
        <span class="pb-skill-fill" style="--pb-skill-w: 95%; --pb-skill-delay: .2s;"></span>
      </span>
      <span class="pb-skill-pct">95%</span>
    </dd>
  </div>
  <div class="pb-skill-row">
    <dt>Figma</dt>
    <dd>
      <span class="pb-skill-rail">
        <span class="pb-skill-fill" style="--pb-skill-w: 78%; --pb-skill-delay: .3s;"></span>
      </span>
      <span class="pb-skill-pct">78%</span>
    </dd>
  </div>
</dl>
09 / 12
Circular Counter
Light JS
0 score
A radial chart with a synchronised counting number in the centre. The ring fills via SVG `stroke-dasharray` (with `pathLength="100"` so the math is honest) while the number animates from 0 — bound by light JS to keep the number truly in sync.
.pb-count {
  position: relative;
  width: 130px; height: 130px;
  font-family: system-ui, sans-serif;
}
.pb-count-ring {
  width: 100%; height: 100%;
  transform: rotate(-90deg);
}
.pb-count-bg,
.pb-count-prog {
  fill: none;
  stroke-width: 7;
  stroke-linecap: round;
}
.pb-count-bg   { stroke: rgba(255,255,255,0.06); }
.pb-count-prog {
  stroke: url(#pb-count-grad);
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 1.4s cubic-bezier(.5,0,.3,1.2);
}
.pb-count.is-ready .pb-count-prog {
  stroke-dashoffset: calc(100 - (var(--pb-count-pct, 0) * 100));
}
.pb-count-meta {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 4px;
  line-height: 1;
}
.pb-count-meta strong {
  display: block;
  font-size: 28px; font-weight: 700;
  color: #f0eeff;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
}
.pb-count-meta span {
  display: block;
  font-size: 9.5px; font-weight: 600;
  color: #34d399;
  letter-spacing: 0.18em; text-transform: uppercase;
  line-height: 1;
}
<div class="pb-count" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Score" data-pb-count="86">
  <svg class="pb-count-ring" viewBox="0 0 100 100" aria-hidden="true">
    <defs>
      <linearGradient id="pb-count-grad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%"   stop-color="#34d399"/>
        <stop offset="100%" stop-color="#10b981"/>
      </linearGradient>
    </defs>
    <circle class="pb-count-bg"   cx="50" cy="50" r="42"></circle>
    <circle class="pb-count-prog" cx="50" cy="50" r="42" pathLength="100"></circle>
  </svg>
  <div class="pb-count-meta">
    <strong data-pb-count-num>0</strong>
    <span>score</span>
  </div>
</div>
// Reads the target value from data-pb-count and animates the
// SVG ring + centre number from 0 to that value over 1.4s.
// Drop this on every page where you render a .pb-count element.
document.querySelectorAll('[data-pb-count]').forEach(function (el) {
  var target = Number(el.dataset.pbCount) || 0;
  var num    = el.querySelector('[data-pb-count-num]');

  // Drive the ring's stroke-dashoffset via a CSS custom property
  el.style.setProperty('--pb-count-pct', String(target / 100));
  requestAnimationFrame(function () { el.classList.add('is-ready'); });

  // Tick the centre number in sync with the 1.4s ring transition
  var start = null;
  var duration = 1400;
  function tick(t) {
    if (start === null) start = t;
    var p = Math.min(1, (t - start) / duration);
    var eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
    var v = Math.round(target * eased);
    if (num) num.textContent = v;
    el.setAttribute('aria-valuenow', String(v));
    if (p < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
});
10 / 12
Stacked Categories
Pure CSS
A single bar split into multiple coloured segments — Documents, Photos, Apps, Free. Each segment's width is a CSS variable, with a legend below. Premium pattern for storage dashboards and budget breakdowns.
.pb-stack {
  width: 260px;
  display: grid; gap: 10px;
  font-family: system-ui, sans-serif;
}
.pb-stack-head {
  display: flex; align-items: baseline; justify-content: space-between;
  color: #f0eeff;
}
.pb-stack-head strong {
  font-size: 18px; font-weight: 700;
  letter-spacing: -0.01em;
}
.pb-stack-head span { font-size: 12px; color: #94a3b8; }
.pb-stack-rail {
  display: flex; height: 10px;
  background: rgba(255,255,255,0.05);
  border-radius: 99px;
  overflow: hidden;
  gap: 2px;
  padding: 0 1px;
}
.pb-stack-seg {
  height: 100%;
  border-radius: 99px;
  transition: width 0.6s cubic-bezier(.5,0,.3,1.2);
  width: var(--w, 0%);
}
.pb-stack-seg.s1 { background: linear-gradient(90deg, #6366f1, #818cf8); }
.pb-stack-seg.s2 { background: linear-gradient(90deg, #ec4899, #f472b6); }
.pb-stack-seg.s3 { background: linear-gradient(90deg, #14b8a6, #2dd4bf); }
.pb-stack-seg.s4 { background: rgba(255,255,255,0.12); }
.pb-stack-legend {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px;
  font-size: 11px;
  color: #cbd5e1;
}
.pb-stack-legend li {
  display: flex; align-items: center; gap: 6px;
}
.pb-stack-legend li i {
  width: 8px; height: 8px;
  border-radius: 2px;
  display: inline-block;
}
.pb-stack-legend li.s1 i { background: #818cf8; }
.pb-stack-legend li.s2 i { background: #f472b6; }
.pb-stack-legend li.s3 i { background: #2dd4bf; }
.pb-stack-legend li.s4 i { background: rgba(255,255,255,0.3); }
.pb-stack-legend em {
  font-style: normal; margin-left: auto;
  color: #94a3b8;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10.5px;
}
<div class="pb-stack" role="img" aria-label="Storage usage breakdown">
  <div class="pb-stack-head">
    <strong>128 GB used</strong>
    <span>of 256 GB</span>
  </div>
  <div class="pb-stack-rail">
    <span class="pb-stack-seg s1" style="--w: 28%;" aria-label="Documents 28%"></span>
    <span class="pb-stack-seg s2" style="--w: 18%;" aria-label="Photos 18%"></span>
    <span class="pb-stack-seg s3" style="--w: 14%;" aria-label="Apps 14%"></span>
    <span class="pb-stack-seg s4" style="--w: 40%;" aria-label="Free 40%"></span>
  </div>
  <ul class="pb-stack-legend">
    <li class="s1"><i></i>Documents <em>36 GB</em></li>
    <li class="s2"><i></i>Photos <em>23 GB</em></li>
    <li class="s3"><i></i>Apps <em>18 GB</em></li>
    <li class="s4"><i></i>Free <em>51 GB</em></li>
  </ul>
</div>
11 / 12
Speed Test Gauge
Pure CSS
An SVG semicircular speedometer with a colour-graded arc (red → amber → green), tick marks at 0/50/100, and a needle that sweeps from the bottom. Drives every value via the `--pb-gauge-value` custom property — premium pattern for PageSpeed scores, performance audits, and internet speed tests.
.pb-gauge {
  width: 220px; height: 170px;
  font-family: system-ui, sans-serif;
}
.pb-gauge-svg {
  width: 100%; height: 100%;
  display: block;
  overflow: visible;
}
.pb-gauge-value {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: pbGaugeArc 1.4s cubic-bezier(.5,0,.3,1.2) forwards;
}
@keyframes pbGaugeArc {
  to { stroke-dashoffset: calc(100 - var(--pb-gauge-value, 0)); }
}
.pb-gauge-needle {
  transform-origin: 100px 105px;
  animation: pbGaugeSweep 1.4s cubic-bezier(.5,0,.3,1.2) forwards;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
}
@keyframes pbGaugeSweep {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(calc(var(--pb-gauge-value, 0) * 1.8deg - 90deg)); }
}
.pb-gauge-tick {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px; font-weight: 700;
  fill: #64748b;
  letter-spacing: 0.04em;
}
.pb-gauge-num {
  font-family: system-ui, sans-serif;
  font-size: 30px; font-weight: 700;
  fill: #34d399;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.pb-gauge-label {
  font-family: system-ui, sans-serif;
  font-size: 10px; font-weight: 600;
  fill: #94a3b8;
  letter-spacing: 0.16em;
}
@media (prefers-reduced-motion: reduce) {
  .pb-gauge-value  { animation: none; stroke-dashoffset: calc(100 - var(--pb-gauge-value, 0)); }
  .pb-gauge-needle { animation: none; transform: rotate(calc(var(--pb-gauge-value, 0) * 1.8deg - 90deg)); }
}
<div class="pb-gauge" role="progressbar" aria-valuenow="78" aria-valuemin="0" aria-valuemax="100" aria-label="Performance score" style="--pb-gauge-value: 78;">
  <svg class="pb-gauge-svg" viewBox="0 0 200 170" aria-hidden="true">
    <defs>
      <linearGradient id="pb-gauge-grad" x1="0" y1="0" x2="1" y2="0">
        <stop offset="0%"   stop-color="#f43f5e"/>
        <stop offset="50%"  stop-color="#fb923c"/>
        <stop offset="100%" stop-color="#34d399"/>
      </linearGradient>
    </defs>
    <!-- track -->
    <path class="pb-gauge-track" d="M 25 105 A 75 75 0 0 1 175 105"
          fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="12" stroke-linecap="round"/>
    <!-- value arc -->
    <path class="pb-gauge-value" d="M 25 105 A 75 75 0 0 1 175 105"
          fill="none" stroke="url(#pb-gauge-grad)" stroke-width="12" stroke-linecap="round"
          pathLength="100"/>
    <!-- ticks -->
    <text class="pb-gauge-tick" x="25"  y="125" text-anchor="middle">0</text>
    <text class="pb-gauge-tick" x="100" y="22"  text-anchor="middle">50</text>
    <text class="pb-gauge-tick" x="175" y="125" text-anchor="middle">100</text>
    <!-- needle -->
    <g class="pb-gauge-needle">
      <line x1="100" y1="105" x2="100" y2="44" stroke="#f0eeff" stroke-width="3" stroke-linecap="round"/>
      <circle cx="100" cy="105" r="7" fill="#fff" stroke="#15151d" stroke-width="2"/>
    </g>
    <!-- score readout -->
    <text class="pb-gauge-num"   x="100" y="148" text-anchor="middle">78</text>
    <text class="pb-gauge-label" x="100" y="164" text-anchor="middle">PERFORMANCE</text>
  </svg>
</div>
12 / 12
Buffer Bar
Pure CSS
1:24 4:18
Penthouse Tour · 4K HD · 1080p
Video-player progress bar with two filled regions: solid played + lighter buffered ahead. A subtle scrubber dot sits at the play head — premium pattern for media players, podcast UIs, and audio dashboards.
.pb-buf {
  width: 280px;
  display: grid; gap: 8px;
  font-family: system-ui, sans-serif;
}
.pb-buf-time {
  display: flex; justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; color: #f0eeff;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.pb-buf-total { color: #94a3b8; }
.pb-buf-rail {
  position: relative;
  height: 5px;
  background: rgba(255,255,255,0.08);
  border-radius: 99px;
}
.pb-buf-buffered {
  position: absolute; inset: 0;
  width: var(--pb-buf-buffered, 0%);
  background: rgba(255,255,255,0.18);
  border-radius: inherit;
  transition: width 0.4s ease;
}
.pb-buf-played {
  position: absolute; inset: 0;
  width: var(--pb-buf-played, 0%);
  background: linear-gradient(90deg, #ef4444, #f97316);
  border-radius: inherit;
  transition: width 0.2s linear;
}
.pb-buf-thumb {
  position: absolute; top: 50%;
  left: var(--pb-buf-played, 0%);
  width: 12px; height: 12px;
  margin-left: -6px; margin-top: -6px;
  background: #fff;
  border-radius: 50%;
  box-shadow:
    0 0 0 3px rgba(239,68,68,0.4),
    0 2px 8px rgba(0,0,0,0.5);
  transform: scale(0);
  transition: transform 0.2s ease, left 0.2s linear;
}
.pb-buf-rail:hover .pb-buf-thumb,
.pb-buf:focus-within .pb-buf-thumb { transform: scale(1); }
.pb-buf-meta {
  display: flex; justify-content: space-between; align-items: center;
}
.pb-buf-title {
  font-size: 12px; font-weight: 600;
  color: #f0eeff;
}
.pb-buf-quality {
  font-size: 10px; color: #94a3b8;
  font-weight: 600;
  background: rgba(255,255,255,0.06);
  padding: 2px 6px; border-radius: 4px;
  letter-spacing: 0.04em;
}
<div class="pb-buf" role="progressbar" aria-valuenow="42" aria-valuemin="0" aria-valuemax="100" aria-label="Playback progress">
  <div class="pb-buf-time">
    <span>1:24</span>
    <span class="pb-buf-total">4:18</span>
  </div>
  <div class="pb-buf-rail">
    <span class="pb-buf-buffered" style="--pb-buf-buffered: 68%;"></span>
    <span class="pb-buf-played"   style="--pb-buf-played: 42%;"></span>
    <span class="pb-buf-thumb"    style="--pb-buf-played: 42%;"></span>
  </div>
  <div class="pb-buf-meta">
    <span class="pb-buf-title">Penthouse Tour · 4K</span>
    <span class="pb-buf-quality">HD · 1080p</span>
  </div>
</div>
FAQ

Frequently asked questions

What is a CSS progress bar?
A progress bar is a visual indicator showing how much of a task or value has been completed against a target. It can be linear (a horizontal bar), circular (a ring or arc), or segmented (discrete steps). Used for file uploads, form completion, skill levels, fundraising goals, and storage usage.
Should I use the native HTML progress element?
Yes when you can. Native progress is the most accessible option — assistive tech announces the value automatically. The patterns here use either a real progress element or a labelled SVG with role=progressbar plus aria-valuenow / aria-valuemin / aria-valuemax for the same semantics.
How do I make a circular CSS progress bar?
Two common approaches: (1) an SVG circle with stroke-dasharray and pathLength=100 — animate stroke-dashoffset from 100 to (100 - value); or (2) a conic-gradient with @property animation — animate the angle from 0deg to value*3.6deg. Both work without JavaScript for static values.
Are these progress bars accessible?
Yes. Each demo uses either a native progress element or a div with role=progressbar plus aria-valuenow, aria-valuemin, aria-valuemax and an associated label. Continuous animations (stripes, gradient pulse) honour the prefers-reduced-motion media query.
Do these progress bars need JavaScript?
Most don't. The visual fill is driven by a single CSS custom property (--value) that you set inline or with one line of JS. Only the Circular Counter needs ~20 lines of JS to keep the centre number in sync with the ring animation; the rest are pure CSS.

Related collections