Needle Gauge
Half-dial needle gauge: five coloured zones and a needle that swings, with a slight bounce, to the value set in data-value.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Data has 28 effects, including 1 free. Explore the category →
3 usage examples



How it works
The .ndg-arc dial is a box twice as wide as it is tall with rounded top corners: a conic-gradient centred at the bottom middle paints five zones (red, orange, green, orange, red) across 180°, and a radial-gradient mask hollows out the middle. The hole is truly transparent, so no dark disc is left behind on a light page.
The .ndg-needle rotates around its foot, which sits on the .ndg-hub at the bottom centre of the dial. The script computes (value − min) / (max − min) × 180° − 90°, and the transform 1s cubic-bezier(.34, 1.56, .64, 1) transition makes it overshoot slightly before settling. An out-of-range value is printed as is while the needle stops at the end of the dial.
Everything is set in the HTML: data-value, data-min and data-max (0 and 100 by default), data-unit, data-decimals, data-label. A MutationObserver watches these attributes, so root.setAttribute('data-value', 42) is enough to move the needle, with no global function. Each .fx-0244-root is self-contained and several gauges can share a page.
When the gauge scrolls into view (IntersectionObserver, 35% threshold) the needle starts from the left stop and sweeps to the value. The number and the min / middle / max ticks are formatted by Intl.NumberFormat in the page language (lang).
Accessibility
- Meter role: the
.ndg-gaugeblock getsrole="meter"witharia-valuemin,aria-valuemax,aria-valuenowandaria-valuetext(formatted value and unit); its name comes fromdata-label. The dial and ticks arearia-hidden. - Colour backed by a number: the zone under the needle is never the only cue, the value is always written below the dial. If your zones carry meaning (too dry, ideal…), state it in text next to the gauge too.
- Reduced motion: under
prefers-reduced-motion: reducethe needle lands directly on the value, with no sweep or bounce, on first display and on every change. - No interaction of its own: the gauge displays a value, it is not a control. If users must pick the value, give them a real
<input type="range">that updatesdata-value, as in the coffee example.
Browser compatibility
Relies on conic-gradient, mask-image (plus its -webkit- form), aspect-ratio, IntersectionObserver and MutationObserver. Measured in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): needle visible at rest and after a change, two independent gauges on one page, reduced motion, 390 px width, white background, no JS error.
Without JavaScript the coloured dial, the value and the ticks written in the HTML stay visible; the needle rests at the left stop. Without IntersectionObserver the needle goes straight to the value.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0244-root" data-value="72" data-min="0" data-max="100" data-label="Level">
<div class="ndg-gauge">
<div class="ndg-dial" aria-hidden="true">
<div class="ndg-arc"></div>
<div class="ndg-needle"></div>
<div class="ndg-hub"></div>
</div>
<div class="ndg-value">72</div>
<div class="ndg-scale" aria-hidden="true">
<span class="ndg-min">0</span>
<span class="ndg-mid">50</span>
<span class="ndg-max">100</span>
</div>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
data-value, data-min, data-max (HTML, on the root) |
72, 0, 100 | Value and bounds. Changing data-value from JS swings the needle; the ticks show min, middle and max. |
data-unit and data-decimals (HTML) |
no unit, 0 decimals | % gives a percentage formatted for the language ("72%" / "72 %"); any other text is appended after the number ("12 cents"). Up to 3 decimals. |
data-label (HTML) |
"Level" | Gauge name read by screen readers. |
--ndg-z1 to --ndg-z4 (CSS) |
20, 40, 80, 90 | Thresholds of the five zones as a percentage of the arc: red up to z1, orange up to z2, green up to z3, orange up to z4, red after that. |
--ndg-low, --ndg-warn, --ndg-good (CSS) |
#ef4444, #f59e0b, #22c55e | Colours of the outer, intermediate and central zones. |
--ndg-size and --ndg-hole (CSS) |
240px, 78% | Maximum dial width (it shrinks with its container) and hole radius: 60% gives a thick arc, 88% a thin one. |
--ndg-needle, --ndg-accent, --ndg-muted (CSS) |
indigo-violet gradient, #a78bfa, 50% white | Needle, hub and number, ticks. Darken them on a light background. |
--ndg-speed (CSS) |
1s | Duration of the needle swing. |
--ndg-bg and --ndg-min-height (CSS) |
#0a0a0f, 300px | Root background and minimum height: transparent and 0 to fit the gauge inside a card. |
FAQ
document.querySelector('#my-gauge').setAttribute('data-value', reading). The script observes the attribute and swings the needle; no function call or global variable. Set data-min and data-max to the scale of your readings (−50 to 50, 0 to 3,000…).cubic-bezier(.34, 1.56, .64, 1) curve: its second value above 1 produces a bounce. For a movement without overshoot, replace it with ease-out in the .ndg-needle rule. The bounce disappears on its own for visitors who asked for less motion.--ndg-z1 to --ndg-z4 are percentages of the arc. For a narrow centred green: 12, 40, 60, 88. For "higher is better": 20, 40, 100, 100 (red, orange, then green to the end). Colours change with --ndg-low, --ndg-warn and --ndg-good.