Meter
stimeo--meter
A meter for a level right now, such as disk usage or battery, banded by thresholds.
Shows how much of a known range is used right now, such as disk space or battery, which is a different thing from progress moving towards completion. The amount reaches screen readers, and the fraction is readable from CSS for filling a bar. Set a low and a high threshold and which band the value is in is published for CSS too. Because a band told by colour alone leaves people out, the spoken form can carry wording such as "85 percent, running low". The value can be set from markup.
Keyboard
This component has no keyboard interactions of its own.
<%# Markup for the meter demo.
Syncs role="meter" and aria-value*, and classifies data-state into low/medium/high
via the low/high thresholds. demo.css applies color based on data-state.
aria-valuetext is generated by the library from the {percent}/{state} template.
Only a move into another segment reaches the shared stimeo--announcer your app seats
once, in its layout — reading every value would be unusable.
Like a progressbar, a meter must not contain interactive children, so the preset
buttons sit beside it and update it through the meter:set CustomEvent the
controller listens for (see demo.js) rather than a Stimulus data-action, which
only binds within the controller's own element. %>
<div class="meter-demo">
<div
class="meter"
data-controller="stimeo--meter"
role="meter"
aria-label="<%= t('components.meter.demo.label') %>"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="72"
data-stimeo--meter-value-value="72"
data-stimeo--meter-low-value="50"
data-stimeo--meter-high-value="80"
data-stimeo--meter-value-text-value="<%= t('components.meter.demo.value_text') %>"
data-stimeo--meter-announce-text-value="<%= t('components.meter.demo.announce_state') %>"
data-action="meter:set->stimeo--meter#setValue">
<div class="meter__bar" data-stimeo--meter-target="bar"></div>
</div>
<div class="meter-demo__controls" role="group"
aria-label="<%= t('components.meter.demo.controls_label') %>">
<% [20, 65, 90].each do |amount| %>
<button
class="demo-trigger"
type="button"
data-meter-amount="<%= amount %>">
<%= amount %>
</button>
<% end %>
</div>
</div>
/*
* Presentation-only styles for the meter demo.
* The bar width comes from --stimeo--meter-ratio (0–1) and its color from the
* data-state band (low / medium / high). aria-valuetext is used alongside color
* so meaning never relies on color alone.
*/
.meter-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.meter {
height: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface-subtle);
overflow: hidden;
}
.meter__bar {
height: 100%;
border-radius: inherit;
width: calc(var(--stimeo--meter-ratio, 0) * 100%);
background: var(--slate-500);
transition:
width 0.2s ease,
background-color 0.2s ease;
}
.meter[data-state="low"] .meter__bar {
background: var(--leaf-500);
}
.meter[data-state="medium"] .meter__bar {
background: var(--amber-500);
}
.meter[data-state="high"] .meter__bar {
background: var(--danger-500);
}
.meter-demo__controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
@media (prefers-reduced-motion: reduce) {
.meter__bar {
transition: none;
}
}
// meter external-update demo (consumer-side JS).
//
// The core controller (stimeo--meter) syncs the ARIA value attributes and the
// low/medium/high data-state; it does not own any buttons. A meter must not
// contain interactive children, so the preset buttons live next to it, and a
// Stimulus data-action on them would never bind (actions only wire up within the
// controller's own element). The contract for updating from outside is the
// meter:set event, so here each button fires meter:set with its value.
document.querySelectorAll(".meter-demo").forEach((root) => {
const meter = root.querySelector('.meter[data-controller~="stimeo--meter"]');
const controls = root.querySelector(".meter-demo__controls");
if (!meter || !controls) return;
controls.querySelectorAll("[data-meter-amount]").forEach((button) => {
button.addEventListener("click", () => {
const value = Number(button.getAttribute("data-meter-amount"));
meter.dispatchEvent(new CustomEvent("meter:set", { detail: { value } }));
});
});
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--meter"
Targets
| Name | Description | Attribute |
|---|---|---|
bar
|
The fill element the consumer styles from the ratio custom property. | data-stimeo--meter-target="bar" |
Values
| Name | Description | Attribute |
|---|---|---|
announceText
|
Wording read out when the segment changes, substituting {value}/{state}; empty announces nothing. |
data-stimeo--meter-announce-text-value |
value
|
Current measured value, clamped into [min, max] (default 0). | data-stimeo--meter-value-value |
min
|
Lower bound of the range, reflected to aria-valuemin (default 0). |
data-stimeo--meter-min-value |
max
|
Upper bound of the range, reflected to aria-valuemax (default 100). |
data-stimeo--meter-max-value |
low
|
Threshold at/below which the value is the low segment; active only when the attribute is present. |
data-stimeo--meter-low-value |
high
|
Threshold at/above which the value is the high segment; active only when the attribute is present. |
data-stimeo--meter-high-value |
optimum
|
Optimal value within the range (declared per the meter role; default 0). | data-stimeo--meter-optimum-value |
valueText
|
Template for aria-valuetext, substituting {value}/{percent}/{state}; empty clears it. |
data-stimeo--meter-value-text-value |
Actions
| Name | Description | Action |
|---|---|---|
setValue
|
Sets the value from an action param (amount) or detail.value, syncs ARIA and data-state. |
stimeo--meter#setValue |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires after setValue with detail.value, detail.ratio, and detail.state (segment). |
stimeo--meter:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-valuenow |
Root element | The current measured value. |
--stimeo--meter-ratio |
Root element | The 0–1 fraction your CSS turns into the bar width. |
data-state |
Root element | "low" / "medium" / "high" threshold segment. |