demo · v130

Segmented Meter

A single <meter appearance: none> can look like a battery indicator, a VU level meter, or a star rating — all via a repeating-linear-gradient overlay that cuts the fill into discrete segments. Chrome 130's fallback styles make the box model usable so the technique actually works.

① Battery bars — 10 segments

Active segments are emerald; if the value drops to or below the low threshold the fill turns rose via ::-webkit-meter-even-less-good-value. The gap lines are a repeating-linear-gradient applied to the meter background, then duplicated on a ::after pseudo-element so they sit above the fill.

7 / 10
7 of 10

② Audio levels — 20 segments

Twenty thin segments with a fill gradient that shifts from emerald (quiet) through blue (mid) to rose (peak). Drag past the high threshold to push into the red zone — the fill pseudo-element changes class and the gradient clips accordingly.

11 / 20
11 of 20

③ Star rating — 5 segments

Five wide segments styled with a blue fill. Click a star glyph below the bar to jump to that value, or use the slider. The <meter> element does the bookkeeping; the star labels are decorative spans wired to the same value.

3 / 5
3 of 5

how it works

The segmentation is an optical illusion: a repeating-linear-gradient draws opaque gap lines at regular intervals over the meter track. The fill pseudo-element sits behind those lines, so as the value grows it fills more segments without the CSS knowing anything about “segments” — the browser's native value-to-width mapping does the math.

Because Chrome 130's fallback styles give appearance: none meters a real box model, the overlay ::after pseudo-element can use position: absolute; inset: 2px to sit flush inside the border and cover the fill precisely.

the code

/* ── Core technique: segment gaps via repeating-linear-gradient ── */

meter.battery {
  appearance: none;
  width: 100%;
  height: 1.4rem;
  display: block;
  border: 2px solid var(--border-black);

  /* Two layers: gap pattern on top, stone track below */
  background:
    repeating-linear-gradient(
      to right,
      transparent 0,
      transparent calc((100% / 10) - 2px),     /* segment body */
      var(--bg-paper) calc((100% / 10) - 2px),  /* gap start    */
      var(--bg-paper) calc(100% / 10)           /* gap end      */
    ),
    var(--bg-stone);
}

/* Strip the default Webkit bar so our background is the track */
meter.battery::-webkit-meter-bar { background: transparent; }

/* Fill colour for "good" range */
meter.battery::-webkit-meter-optimum-value {
  background: var(--accent-emerald);
}
/* Fill colour when value <= low threshold */
meter.battery::-webkit-meter-even-less-good-value {
  background: var(--accent-rose);
}

/* Overlay wrapper: an ::after pseudo stacked above the fill */
.battery-wrap { position: relative; }
.battery-wrap::after {
  content: "";
  position: absolute;
  inset: 2px;           /* inside the 2px border */
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to right,
      transparent 0,
      transparent calc((100% / 10) - 2px),
      var(--bg-paper) calc((100% / 10) - 2px),
      var(--bg-paper) calc(100% / 10)
    );
}

/* ── Audio: 20 segments + gradient fill ── */
meter.audio::-webkit-meter-optimum-value {
  background: linear-gradient(
    to right,
    var(--accent-emerald) 0%,
    var(--accent-emerald) 50%,
    var(--accent-blue)    70%,
    var(--accent-rose)    100%
  );
}

see also