Back to CSS Progress Bars Speed Test Gauge Pure CSS
Share
HTML
<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>
CSS
.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(0.5, 0, 0.3, 1.2) forwards;
}

.pb-gauge-needle {
  transform-origin: 100px 105px;
  animation: pbGaugeSweep 1.4s cubic-bezier(0.5, 0, 0.3, 1.2) forwards;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
}

.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));
  }
}

@keyframes pbGaugeArc {
  to { stroke-dashoffset: calc(100 - var(--pb-gauge-value, 0)); }
}

@keyframes pbGaugeSweep {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(calc(var(--pb-gauge-value, 0) * 1.8deg - 90deg)); }
}