Gauge
A dial that reads one number against a range - a CPU meter, a disk-usage ring, or the speedometer and tachometer off a car's dashboard.
Basic
Zero to a hundred, drawn as a filled arc with the reading in the middle. Drag the slider and the dial sweeps to the new value rather than jumping - the arc is revealed with a dash offset precisely so it can be animated.
@* The gauge reads the value; the slider is what moves it in this demo. *@
<VetoGauge Value="_cpu" Label="CPU" Unit="%" />
<VetoSlider TValue="double" Label="Value"
Min="0" Max="100" ShowValue="true"
@bind-Value="_cpu" />
@code {
private double _cpu = 42;
}A car, revving
Two dials driven by one small engine model rather than by a sine wave: the revs climb, hit the limiter, and fall away as each gear goes in while the speed keeps building. Seven gears to 350 km/h, then it holds top speed for five seconds and goes back to the line. Each gauge is given a transition equal to the model's own 40ms step, so one sweep finishes exactly as the next begins - the default 150ms ease would leave both needles permanently chasing a value they never catch.
@* Seven gears, a limiter at 8000, and a top speed it holds for five seconds. *@
@* FlickerFrom: neon flicker once it is into the red.
TransitionDuration: the model's own step, or the needle trails its input. *@
<VetoGauge Needle="true" Value="@_rpm" Max="8000" Label="RPM x1000"
Bands="RpmBands" FollowBands="true"
FlickerFrom="7200"
TransitionDuration="40"
Background="radial-gradient(circle at 50% 42%, #1b212c, #0b0e13)">
<FaceContent>
<span class="badge">VETO <small>reto spec</small></span>
</FaceContent>
</VetoGauge>
<VetoGauge Needle="true" Value="@_speed" Max="360" Label="km/h"
TransitionDuration="40" />
@code {
const double TopSpeed = 350, Redline = 8000, Idle = 850;
const double HoldSeconds = 5, ShiftSeconds = 0.28;
// Revs per km/h in each gear, and how hard each one pulls in km/h per second.
static readonly double[] Ratios = [118, 80, 58, 44, 34, 27, 21.5];
static readonly double[] Pull = [60, 48, 39, 31, 24, 18, 14];
double _speed, _rpm = Idle, _shifting, _holding;
int _gear = 1;
void Advance(double seconds)
{
if (_holding > 0)
{
// Sitting on the top speed, then back to the line.
_holding -= seconds;
if (_holding <= 0) Reset();
}
else if (_shifting > 0)
{
// The clutch is out: no drive, so the car coasts through the shift.
_shifting -= seconds;
_speed = Math.Max(0, _speed - 3 * seconds);
}
else
{
// Pull falls away with speed, so it creeps up on its top speed
// instead of marching to it at a constant rate.
_speed += Pull[_gear - 1] * (1 - 0.6 * (_speed / TopSpeed)) * seconds;
if (_speed >= TopSpeed)
{
_speed = TopSpeed;
_holding = HoldSeconds;
}
}
_rpm = Math.Clamp(_speed * Ratios[_gear - 1] + Idle, Idle, Redline);
// At the limiter the next gear goes in, and the revs fall to whatever
// that ratio makes of the speed the car is already carrying.
if (_rpm >= Redline && _gear < Ratios.Length
&& _shifting <= 0 && _holding <= 0)
{
_gear++;
_shifting = ShiftSeconds;
}
}
}Bands, and a dial that colours itself
Bands colour stretches of the dial - the amber zone before a disk fills up, the red line on a tachometer. With FollowBands the reading itself takes the colour of whichever band it falls in, so the gauge turns red on its own rather than waiting to be told.
<VetoGauge Value="_disk" Label="Disk" Unit="%"
Bands="DiskBands" FollowBands="true" />
<VetoSlider TValue="double" Label="Usage"
Min="0" Max="100" ShowValue="true"
@bind-Value="_disk" />
@code {
private double _disk = 55;
private static readonly VetoGaugeBand[] DiskBands =
[
new(0, 70, "var(--veto-color-success)", "Healthy"),
new(70, 90, "var(--veto-color-warning)", "Filling up"),
new(90, 100, "var(--veto-color-danger)", "Nearly full"),
];
}Needle, ticks and a car's dashboard
Needle swaps the filled arc for a swinging pointer, which is the shape the instrument in front of a driver actually takes. ShowTicks marks the scale and ShowTickLabels numbers the major ones. The tachometer below carries a red line and follows it.
<VetoGauge Needle="true"
Value="_rpm"
Min="0" Max="8000"
Label="RPM" Format="N0"
Size="VetoSize.Large"
ValueFontSize="1.5rem"
ShowTicks="true" TickEvery="250" MajorTickEvery="4"
ShowTickLabels="true"
ValueFontSize="1.5rem"
Bands="RpmBands"
FollowBands="true" />
<VetoSlider TValue="double" Label="RPM"
Min="0" Max="8000" Step="50" ShowValue="true"
@bind-Value="_rpm" />
@code {
private double _rpm = 3200;
// Two bands, in order: amber first, then the red it runs into.
private static readonly VetoGaugeBand[] RpmBands =
[
new(6500, 7200, "var(--veto-color-warning)", "Approaching the line"),
new(7200, 8000, "var(--veto-color-danger)", "Red line"),
];
}A target on the dial
Target marks a goal or a limit straight across the track - the number a sales dial is chasing, the redline on a rev counter. It is read out with the value, so a screen reader hears how far off it is too.
<VetoGauge Label="Quarterly sales" Value="64" Target="80" Unit="%" />
<VetoGauge Label="RPM ×1000" Min="0" Max="8" Value="5.2"
Target="6.5" TargetColor="var(--veto-color-danger)" Needle="true" />Live values
Two ways in, and they layer. ValueProvider is a function the gauge calls every PollInterval - use it when the number comes from a hub, a counter, or an endpoint that needs your own headers. PollUrl is the shortcut for the common case: the gauge GETs that URL with the app's HttpClient and reads a number out of it, optionally at PollJsonPath. Neither polls at all until PollInterval is positive. The gauge below is driven by a provider that makes up a plausible CPU trace.
@* A function the gauge calls on a timer - your HttpClient, your auth, your shape. *@
<VetoGauge Label="CPU" Unit="%"
ValueProvider="NextSampleAsync"
PollInterval="1000"
OnValueChanged="_ => _samples++" />
<VetoButton OnClick="() => _running = !_running">
@(_running ? "Pause" : "Resume")
</VetoButton>
@code {
private bool _running = true;
private int _samples;
private async Task<double> NextSampleAsync()
=> _running ? await Http.GetFromJsonAsync<double>("/api/cpu") : _last;
}
@* Or the shortcut, when the endpoint is plain: *@
<VetoGauge PollUrl="/api/metrics"
PollJsonPath="cpu.percent"
PollInterval="2000" />Tuning the dial
The needle, the hub it turns on and the printed reading are all adjustable. A long unit - km/h - makes the default reading too wide for the dial, so ValueFontSize brings it back inside; TickLabelSize and TickLabelOffset do the same for a crowded scale. Ticks and their numbers are drawn inside the arc, which is both what a car's instrument looks like and what keeps them inside the drawing.
@* A long unit needs a smaller reading, or it runs out over the ticks. *@
<VetoGauge Needle="true" Value="140" Max="240"
Label="Speed" Unit=" km/h"
Size="VetoSize.Large"
ValueFontSize="1.125rem"
LabelFontSize="0.75rem"
ShowTicks="true" TickEvery="20" MajorTickEvery="2"
ShowTickLabels="true" TickLabelSize="9" TickLabelOffset="18"
NeedleWidth="5"
NeedleColor="var(--veto-color-danger)"
HubRadius="10"
HubColor="var(--veto-color-text)" />
@* HubRadius="0" draws no hub at all. *@
<VetoGauge Needle="true" Value="70" NeedleWidth="2" HubRadius="0" />The dial face
Background takes any CSS background - a URL, a data URI, a gradient - clipped to the dial's circle. FaceContent puts markup in the middle of it. Both sit under everything the gauge draws, so the ticks and the needle sweep across the face the way they do on a real instrument, and both are hidden from assistive technology: the reading is what a gauge is for, and a badge printed on it is not part of that.
<VetoGauge Needle="true" Value="18" Max="30" Label="Boost" Unit=" psi"
Background="radial-gradient(circle at 50% 40%, #1b212c 0%, #0e1116 70%)">
<FaceContent>
<span class="badge">
<span class="badge-rule"></span> @* a red rule down the left *@
<span>
<span class="badge-name">VETO</span>
<span class="badge-sub">reto spec</span>
</span>
</span>
</FaceContent>
</VetoGauge>
@* Or just a face, with no content on it: *@
<VetoGauge Value="64" Label="Load" Unit="%"
Background="linear-gradient(140deg, #1b212c, #0e1116)" />Sizes, colours and sweep
Three sizes and the library's colour roles. StartAngle and EndAngle are the dial's own geometry: the default pair leaves the bottom open the way a car's instruments do, and a half circle is the usual choice for a dashboard tile.
<VetoGauge Size="VetoSize.Small" Value="34" Label="Small" Unit="%" />
<VetoGauge Value="61" StartAngle="-90" EndAngle="90" Label="Half circle" />
<VetoGauge Value="45" StartAngle="0" EndAngle="360" RoundCaps="false" Label="Full circle" />Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
Value |
double |
0 |
The number the dial is showing. |
Min / Max |
double |
0 / 100 |
The ends of the scale. |
Needle |
bool |
false |
A swinging pointer instead of a filled arc. |
StartAngle / EndAngle |
double |
-135 / 135 |
The dial's sweep, in degrees clockwise from the top. |
Thickness |
double |
14 |
How thick the dial is drawn, in the SVG's own 200-unit box. |
RoundCaps |
bool |
true |
Round the arc's ends. Ignored for a needle. |
Bands |
IReadOnlyList<VetoGaugeBand>? |
null |
Coloured stretches of the dial: From, To, Color and an optional Label. |
FollowBands |
bool |
false |
The reading takes the colour of the band it falls in. |
Color |
VetoColor |
Primary |
The reading's colour when no band applies. |
Size |
VetoSize |
Medium |
ExtraSmall, Small, Medium, Large or ExtraLarge. |
Label / Unit / Format |
string? |
null |
The name under the reading, what it is measured in, and how it is formatted. |
ShowValue |
bool |
true |
Print the reading in the middle of the dial. |
ShowTicks / TickEvery / MajorTickEvery / ShowTickLabels |
bool / double / int / bool |
false / 10 / 5 / false |
The tick marks around the dial. |
TickLabelSize / TickLabelOffset |
double |
11 / 15 |
How big a tick's number is, and how far in from the dial it sits. |
NeedleLength / NeedleWidth / NeedleColor |
double? / double / string? |
null / 3 / null |
The needle. Null length stops it short of the numbers; null colour follows the reading. |
HubRadius / HubColor |
double / string? |
7 / null |
The hub the needle turns on. Zero draws none. |
ValueFontSize / LabelFontSize |
string? |
null |
Any CSS length. Worth setting when a long unit makes the reading too wide for the dial. |
Background |
string? |
null |
The dial's face, as any CSS background. Clipped to the circle, drawn under the ticks and the needle. |
FaceContent |
RenderFragment? |
null |
Markup in the middle of the dial - a badge, a model name. Decorative, and hidden from assistive technology. |
FlickerFrom |
double? |
null |
The reading at or above which the dial flickers like a neon tube. Held steady under prefers-reduced-motion. |
TransitionDuration |
int? |
null |
How long the needle takes to reach a new reading, in ms. Set it to the interval between updates - or 0 - for a dial driven continuously, or it trails its input. |
Target |
double? |
null |
A goal or limit marked across the track. Included in the announced value. |
TargetColor |
string? |
null |
The target marker's colour. Defaults to the text colour. |
ValueProvider |
Func<Task<double>>? |
null |
Called every PollInterval for the current value. The general way in. |
PollUrl / PollJsonPath |
string? |
null |
GET this URL and read the number at this dotted path. Needs an HttpClient registered. |
PollInterval |
int |
0 |
How often to poll, in milliseconds. Zero never polls. |
OnValueChanged / OnPollError |
EventCallback |
— |
Raised after a poll changes the reading, and when one fails. |
RefreshAsync() |
Task |
— |
Fetch the value once, now, whether or not the gauge is polling. |