demo · v130

Animated Meter

Chrome 130 lets appearance: none meters actually render, so the ::-webkit-meter-optimum-value pseudo-element is available to transition. Here, each <meter value> is nudged by a setInterval and the fill bar animates via transition: width 0.1s linear — no canvas, no custom track element required.

how it works

The key technique: set appearance: none on the <meter>, then target the Chromium-specific ::-webkit-meter-optimum-value pseudo-element with a CSS transition. Every 100 ms, JavaScript increments meter.value — the browser recomputes the pseudo-element's width, and the transition handles the smooth interpolation.

Before Chrome 130, appearance: none caused the meter to collapse to zero size. The new fallback styles preserve a usable box model, making this pattern possible without wrapper hacks.

the code

/* Chrome 130: appearance: none now falls back gracefully */
meter {
  appearance: none;
  width: 100%;
  height: 0.6rem;
  background: var(--bg-stone);    /* track colour */
  border: 1px solid var(--border-black);
  display: block;
}

/* Remove the default Webkit bar so our background shows through */
meter::-webkit-meter-bar {
  background: var(--bg-stone);
  border: none;
}

/* The fill pseudo-element — add a CSS transition here */
meter::-webkit-meter-optimum-value {
  background: var(--accent-emerald);  /* emerald fill */
  transition: width 0.1s linear;      /* smooth animation */
}
meter::-webkit-meter-suboptimum-value { background: var(--accent-blue); }
meter::-webkit-meter-even-less-good-value { background: var(--accent-rose); }

/* Firefox */
meter::-moz-meter-bar {
  background: var(--accent-emerald);
  transition: width 0.1s linear;
}

/* JS: nudge value every 100 ms */
const tick = setInterval(() => {
  if (meter.value >= targetPct) {
    clearInterval(tick);
    return;
  }
  meter.value = Math.min(meter.value + stepSize, targetPct);
}, 100);

see also