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.
② 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.
③ 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.
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
- meter element fallback styles — feature index
- <meter> with author styles — basic appearance: none comparison
- Branded Themes — custom colour schemes on <meter>
- Animated Meter — CSS-transitioned fill via JS value updates
- ChromeStatus entry — meter element fallback styles