Meter
stimeo--meter
Syncs a meter's ARIA value attributes and segments it by threshold.
The stimeo--meter controller drives the WAI-ARIA meter role — a point-in-time scalar within a known range (disk usage, battery, score), distinct from Progress. It keeps aria-valuenow / min / max in sync, exposes the fraction as --stimeo-meter-ratio (0–1), and, when low/high thresholds are present, classifies the value into a low/medium/high segment on data-state. Because state must not be conveyed by color alone, a valueText template feeds aria-valuetext so the segment is also text. setValue accepts an amount action param or a meter:set event, and dispatches stimeo--meter:change. Behavior only — the look is owned by this Playground.
<%# 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.
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-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 |
|---|---|---|
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. |