Components
Meter
The native <meter> element (ARIA role="meter") — a gauge of a value within a known range like battery, disk usage, or a score. Set low/high/optimum to color the fill. For task completion use Progress instead.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/meter.json2. Use it
import { Meter } from "@/components/ui/meter"
<label for="battery">Battery</label>
<Meter id="battery" value={0.75} ariaLabel="Battery" />
// zoned gauge — low/high/optimum color the fill
<Meter value={0.62} low={0.25} high={0.85} optimum={0.1}
valuetext="12.4 GB of 16 GB" ariaLabel="Disk usage" />Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { Child } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Meter — shadcn-htmx, htmx v4 + Tailwind v4.
//
// Source of truth:
// shadcn/ui ships no Meter component. This is a native-platform addition
// distinct from Progress: a gauge of a value within a known range
// (battery, disk usage, score) — NOT task completion.
//
// We render the real native <meter> element (implicit role="meter"), so the
// browser supplies the role + value semantics and AT announces the gauge.
// repos/mdn/files/en-us/web/html/reference/elements/meter/index.md
// - value/min/max/low/high/optimum attributes (lines 33-48)
// - "Implicit ARIA role: meter" and "Permitted ARIA roles: No role
// permitted" (lines 123-134) — so we DO NOT set role/aria-value*; the
// element maps value/min/max onto aria-valuenow/min/max itself.
//
// APG: WAI-ARIA Meter pattern
// repos/aria-practices/content/patterns/meter/meter-pattern.html
// - Keyboard Interaction: "Not applicable" (line 46) — a meter is not
// interactive; no JS keyboard contract.
// - Accessible name via aria-labelledby (visible label) or aria-label
// (lines 61-64). We pair with a native <label for> in the docs.
// - aria-valuetext for human-readable values, e.g. "50% (6 hours)
// remaining" (lines 56-59) — surfaced via the `valuetext` prop.
//
// Cross-browser styling note: WebKit/Blink expose ::-webkit-meter-* and
// Gecko exposes ::-moz-meter-bar. Both need `appearance: none` to accept
// custom styles. Those pseudo-elements can't take Tailwind classes, so the
// fill/track theming lives in app/styles/input.css scoped to
// [data-slot="meter"]. Tailwind utilities here only size the box.
const meterBase =
"block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle"
export function meterClasses(opts?: { class?: ClassValue }): string {
return cn(meterBase, opts?.class)
}
type MeterProps = {
// Current value. Clamped by the browser to [min, max].
value: number
min?: number
max?: number
// Lower/upper bounds of the "low" and "high" zones. Combined with optimum
// they drive the optimum / suboptimum / even-less-good fill color.
low?: number
high?: number
optimum?: number
id?: string
// Free-form units hint. The WHATWG HTML spec notes <meter> has no units
// attribute and that title is the only standard way to convey units
// (e.g. title="gigabytes"). repos/whatwg-html/source meter section.
title?: string
// Accessible name — required when there's no linked <label>.
ariaLabel?: string
ariaLabelledby?: string
ariaDescribedby?: string
// Human-readable value for AT (e.g. "12.4 GB of 16 GB"). Falls back to the
// child text content the element already carries.
valuetext?: string
// Fallback text content for legacy AT and as the visible value in browsers
// without <meter> support. Defaults to a "value/max" string.
children?: Child
class?: ClassValue
// htmx — refresh the gauge from the server.
"hx-get"?: string
"hx-post"?: string
"hx-target"?: string
"hx-swap"?: string
"hx-trigger"?: string
}
export function Meter(props: MeterProps) {
const {
value,
min = 0,
max = 1,
low,
high,
optimum,
id,
title,
ariaLabel,
ariaLabelledby,
ariaDescribedby,
valuetext,
children,
class: className,
...rest
} = props
const fallback = children ?? `${value} / ${max}`
return (
<meter
id={id}
data-slot="meter"
title={title}
value={value}
min={min}
max={max}
low={low}
high={high}
optimum={optimum}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
aria-describedby={ariaDescribedby}
aria-valuetext={valuetext}
class={meterClasses({ class: className })}
{...rest}
>
{fallback}
</meter>
)
}
1. Save the file
Copy meter.html into templates/components/.
2. Use it
{% from "components/meter.html" import meter %}
<label for="battery">Battery</label>
{{ meter(id="battery", value=0.75, aria_label="Battery") }}
{# zoned gauge #}
{{ meter(value=0.62, low=0.25, high=0.85, optimum=0.1,
value_text="12.4 GB of 16 GB", aria_label="Disk usage") }}View source
{# Meter macro — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/meter.tsx. Renders the native <meter> element
(implicit role="meter"); cross-browser fill/track theming lives in
input.css scoped to [data-slot="meter"].
Usage:
{% from "components/meter.html" import meter %}
<label for="disk" class="text-sm font-medium">Disk usage</label>
{{ meter(id="disk", value=0.62, low=0.25, high=0.85, optimum=0.1,
value_text="12.4 GB of 16 GB") }}
#}
{% macro meter(
value=0,
min=0,
max=1,
low=none,
high=none,
optimum=none,
id=none,
title=none,
aria_label=none,
aria_labelledby=none,
aria_describedby=none,
value_text=none,
text=none,
extra_class="",
**attrs
) %}
{%- set base -%}
block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle
{%- endset -%}
<meter class="{{ base }} {{ extra_class }}"
{%- if id %} id="{{ id }}"{% endif %}
{#- title is the spec-sanctioned way to convey units, e.g. "gigabytes" #}
{%- if title %} title="{{ title }}"{% endif %}
value="{{ value }}" min="{{ min }}" max="{{ max }}"
{%- if low is not none %} low="{{ low }}"{% endif %}
{%- if high is not none %} high="{{ high }}"{% endif %}
{%- if optimum is not none %} optimum="{{ optimum }}"{% endif %}
{%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
{%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
{%- if aria_describedby %} aria-describedby="{{ aria_describedby }}"{% endif %}
{%- if value_text %} aria-valuetext="{{ value_text }}"{% endif %}
data-slot="meter"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ text if text is not none else (value ~ ' / ' ~ max) }}</meter>
{% endmacro %}
1. Save the file
Add meter.tmpl alongside your other templates.
2. Use it
{{template "meter" (dict "ID" "battery" "Value" 0.75 "AriaLabel" "Battery")}}
// zoned gauge
{{template "meter" (dict
"Value" 0.62 "Low" 0.25 "High" 0.85 "Optimum" 0.1
"ValueText" "12.4 GB of 16 GB" "AriaLabel" "Disk usage")}}View source
{{/*
Meter template — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/meter.tsx. Renders the native <meter> element
(implicit role="meter"); fill/track theming lives in input.css scoped
to [data-slot="meter"].
Usage:
{{template "meter" (dict
"ID" "disk" "Value" 0.62 "Low" 0.25 "High" 0.85 "Optimum" 0.1
"ValueText" "12.4 GB of 16 GB")}}
Low / High / Optimum are optional; pass them only when zoning the gauge.
*/}}
{{define "meter"}}
{{- $base := "block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle" -}}
{{- $value := or .Value 0 -}}
{{- $min := or .Min 0 -}}
{{- $max := or .Max 1 -}}
{{- $text := or .Text (printf "%v / %v" $value $max) -}}
<meter class="{{$base}}{{if .Class}} {{.Class}}{{end}}"
{{- if .ID}} id="{{.ID}}"{{end}}
{{- /* title: spec-sanctioned way to convey units, e.g. "gigabytes" */}}
{{- if .Title}} title="{{.Title}}"{{end}}
value="{{$value}}" min="{{$min}}" max="{{$max}}"
{{- if .Low}} low="{{.Low}}"{{end}}
{{- if .High}} high="{{.High}}"{{end}}
{{- if .Optimum}} optimum="{{.Optimum}}"{{end}}
{{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
{{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
{{- if .AriaDescribedby}} aria-describedby="{{.AriaDescribedby}}"{{end}}
{{- if .ValueText}} aria-valuetext="{{.ValueText}}"{{end}}
data-slot="meter"
{{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{$text}}</meter>
{{end}}
1. Save the file
Drop meter.ex into lib/my_app_web/components/.
2. Use it
<label for="battery">Battery</label>
<.meter id="battery" value={0.75} aria-label="Battery" />
<.meter value={0.62} low={0.25} high={0.85} optimum={0.1}
value_text="12.4 GB of 16 GB" aria-label="Disk usage" />View source
defmodule ShadcnHtmx.Components.Meter do
@moduledoc """
Meter — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Renders the native `<meter>` element (implicit `role="meter"`) — a gauge
of a value within a known range (battery, disk usage, score), NOT task
progress. For progress use the Progress component instead.
`low` / `high` / `optimum` drive the fill color via the cross-browser
meter pseudo-elements styled in input.css ([data-slot="meter"]).
## Examples
<label for="disk" class="text-sm font-medium">Disk usage</label>
<.meter id="disk" value={0.62} low={0.25} high={0.85} optimum={0.1}
value_text="12.4 GB of 16 GB" />
<.meter value={6} min={0} max={10} aria-label="Score" />
"""
use Phoenix.Component
@base "block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle"
attr :value, :float, default: 0.0
attr :min, :float, default: 0.0
attr :max, :float, default: 1.0
attr :low, :float, default: nil
attr :high, :float, default: nil
attr :optimum, :float, default: nil
attr :value_text, :string, default: nil
attr :text, :string, default: nil
attr :class, :string, default: nil
# title is the spec-sanctioned way to convey units on a meter, e.g.
# title="gigabytes" (WHATWG HTML: <meter> has no units attribute).
attr :rest, :global,
include:
~w(hx-get hx-post hx-put hx-patch hx-target hx-swap hx-trigger hx-vals hx-include
id title aria-label aria-labelledby aria-describedby)
def meter(assigns) do
assigns =
assigns
|> assign(:base, @base)
|> assign(:body, assigns.text || "#{assigns.value} / #{assigns.max}")
~H"""
<meter
class={[@base, @class]}
value={@value}
min={@min}
max={@max}
low={@low}
high={@high}
optimum={@optimum}
aria-valuetext={@value_text}
data-slot="meter"
{@rest}
>{@body}</meter>
"""
end
end
1. Save the file
Paste the markup; fill/track theming lives in the [data-slot="meter"] rules in input.css.
2. Use it
<label for="battery" class="text-sm font-medium">Battery</label>
<meter id="battery" data-slot="meter" value="0.75"
class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">75%</meter>View source
<!--
shadcn-htmx — raw HTML meter snippets.
Mirrors registry/ui/meter.tsx. The native <meter> element carries the
implicit role="meter" (do NOT add role/aria-value*; the browser maps
value/min/max onto aria-valuenow/min/max). Cross-browser fill + track
theming lives in input.css scoped to [data-slot="meter"]; these snippets
only size the box with Tailwind utilities.
BASE: block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle
Pair every <meter> with an accessible name — a linked <label for> (best)
or aria-label.
-->
<!-- Basic — value within a 0..1 range, labelled by a visible <label> -->
<label for="battery" class="text-sm font-medium">Battery</label>
<meter id="battery" data-slot="meter" value="0.75"
class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">75%</meter>
<!-- Zoned — low/high/optimum color the fill (good / warning / danger).
title carries free-form units: the only spec-sanctioned units channel
for <meter> (WHATWG HTML — <meter> has no units attribute). -->
<label for="disk" class="text-sm font-medium">Disk usage</label>
<meter id="disk" data-slot="meter" value="0.62" min="0" max="1"
low="0.25" high="0.85" optimum="0.1" aria-valuetext="12.4 GB of 16 GB"
title="gigabytes"
class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">12.4 GB of 16 GB</meter>
<!-- Custom range + aria-label (no visible label) -->
<meter data-slot="meter" value="6" min="0" max="10" low="3" high="8" optimum="10"
aria-label="Exam score"
class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">6 / 10</meter>
Examples
Basic — a labelled gauge
A value within a known range. Pair every meter with an accessible name — a linked <label for> is best.
The native <meter> carries role="meter" implicitly and maps value/min/max onto aria-valuenow/min/max. Default range is 0–1, so value=0.75 reads as 75%.
<label for="m-battery">Battery</label>
<Meter id="m-battery" value={0.75} ariaLabel="Battery" />
<label for="m-score">Exam score</label>
<Meter id="m-score" value={6} min={0} max={10}
ariaLabel="Exam score" /><label for="m-battery">Battery</label>
{{ meter(id="m-battery", value=0.75, aria_label="Battery") }}
<label for="m-score">Exam score</label>
{{ meter(id="m-score", value=6, min=0, max=10,
aria_label="Exam score") }}{{template "meter" (dict "ID" "m-battery" "Value" 0.75 "AriaLabel" "Battery")}}
{{template "meter" (dict "ID" "m-score" "Value" 6 "Min" 0 "Max" 10 "AriaLabel" "Exam score")}}<.meter id="m-battery" value={0.75} aria-label="Battery" />
<.meter id="m-score" value={6} min={0} max={10} aria-label="Exam score" /><div class="grid w-full max-w-md gap-4">
<div class="grid gap-1.5">
<label for="m-battery" class="text-sm font-medium">Battery</label>
<meter id="m-battery" data-slot="meter" value="0.75" min="0" max="1" aria-label="Battery" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">75%</meter>
</div>
<div class="grid gap-1.5">
<label for="m-score" class="text-sm font-medium">Exam score</label>
<meter id="m-score" data-slot="meter" value="6" min="0" max="10" aria-label="Exam score" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">6 / 10</meter>
</div>
</div>Further reading
Zoned — low / high / optimum
low and high split the range into thirds; optimum tells the browser which end is preferable. The fill is green in the optimal zone, amber when suboptimal, red when far from optimum.
Here optimum=0.1 marks the low end as best (less disk used is better), so a value in the high zone renders red. Move optimum to flip which direction is "good". Set valuetext so AT announces a human-readable value, e.g. "12.4 GB of 16 GB".
<Meter value={0.18} low={0.25} high={0.85} optimum={0.1}
valuetext="18% load" ariaLabel="CPU idle" /> // green
<Meter value={0.62} low={0.25} high={0.85} optimum={0.1}
valuetext="12.4 GB of 16 GB" ariaLabel="Disk" /> // amber
<Meter value={0.94} low={0.25} high={0.85} optimum={0.1}
valuetext="15.0 GB of 16 GB" ariaLabel="Disk" /> // red{{ meter(value=0.18, low=0.25, high=0.85, optimum=0.1,
value_text="18% load", aria_label="CPU idle") }}
{{ meter(value=0.62, low=0.25, high=0.85, optimum=0.1,
value_text="12.4 GB of 16 GB", aria_label="Disk") }}
{{ meter(value=0.94, low=0.25, high=0.85, optimum=0.1,
value_text="15.0 GB of 16 GB", aria_label="Disk") }}{{template "meter" (dict "Value" 0.18 "Low" 0.25 "High" 0.85 "Optimum" 0.1 "ValueText" "18% load" "AriaLabel" "CPU idle")}}
{{template "meter" (dict "Value" 0.62 "Low" 0.25 "High" 0.85 "Optimum" 0.1 "ValueText" "12.4 GB of 16 GB" "AriaLabel" "Disk")}}<.meter value={0.18} low={0.25} high={0.85} optimum={0.1} value_text="18% load" aria-label="CPU idle" />
<.meter value={0.62} low={0.25} high={0.85} optimum={0.1} value_text="12.4 GB of 16 GB" aria-label="Disk" /><div class="grid w-full max-w-md gap-4">
<div class="grid gap-1.5">
<label for="m-low" class="text-sm font-medium">CPU — idle (optimal)</label>
<meter id="m-low" data-slot="meter" value="0.18" min="0" max="1" low="0.25" high="0.85" optimum="0.1" aria-label="CPU idle" aria-valuetext="18% load" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">18%</meter>
</div>
<div class="grid gap-1.5">
<label for="m-mid" class="text-sm font-medium">Disk — getting full (suboptimal)</label>
<meter id="m-mid" data-slot="meter" value="0.62" min="0" max="1" low="0.25" high="0.85" optimum="0.1" aria-label="Disk usage" aria-valuetext="12.4 GB of 16 GB" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">12.4 GB of 16 GB</meter>
</div>
<div class="grid gap-1.5">
<label for="m-high" class="text-sm font-medium">Disk — nearly full (danger)</label>
<meter id="m-high" data-slot="meter" value="0.94" min="0" max="1" low="0.25" high="0.85" optimum="0.1" aria-label="Disk nearly full" aria-valuetext="15.0 GB of 16 GB" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">15.0 GB of 16 GB</meter>
</div>
</div>Further reading
htmx — server-driven live gauge
Poll the server every second; the response is a fresh Meter fragment with the latest reading. A meter (unlike progress) keeps polling — it tracks an ongoing measurement.
Wrap the meter in a container that hx-gets a fragment on hx-trigger="load, every 1s" and swaps outerHTML. Because a gauge measures a live quantity, polling never has to stop — here we just jitter a mock memory reading.
<div hx-get="/api/memory" hx-trigger="load, every 1s" hx-swap="outerHTML">
<Meter value={0.4} low={0.5} high={0.85} optimum={0.1}
ariaLabel="Memory usage" />
</div>
// Server returns a refreshed fragment each tick:
<Meter value={0.63} low={0.5} high={0.85} optimum={0.1}
valuetext="10.1 GB of 16 GB" ariaLabel="Memory usage" /><div hx-get="/api/memory" hx-trigger="load, every 1s" hx-swap="outerHTML">
{{ meter(value=0.4, low=0.5, high=0.85, optimum=0.1, aria_label="Memory usage") }}
</div><div hx-get="/api/memory" hx-trigger="load, every 1s" hx-swap="outerHTML">
{{template "meter" (dict "Value" 0.4 "Low" 0.5 "High" 0.85 "Optimum" 0.1 "AriaLabel" "Memory usage")}}
</div><div hx-get={~p"/api/memory"} hx-trigger="load, every 1s" hx-swap="outerHTML">
<.meter value={0.4} low={0.5} high={0.85} optimum={0.1} aria-label="Memory usage" />
</div><div class="grid w-full max-w-md gap-1.5">
<label for="m-mem" class="text-sm font-medium">Memory (live)</label>
<div hx-get="/meter/tick" hx-trigger="load, every 1s" hx-swap="outerHTML">
<meter id="m-mem" data-slot="meter" value="0.4" min="0" max="1" low="0.5" high="0.85" optimum="0.1" aria-label="Memory usage" class="block h-2 w-full overflow-hidden rounded-full bg-primary/20 align-middle">loading…</meter>
</div>
</div>Further reading
API Reference
<Meter>
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Free-form units hint, e.g. title="gigabytes". Per WHATWG HTML, <meter> has no units attribute and the global title is the only standard way to convey units. Forwarded to the native element.MDN<meter> global attributes |
value* | number | — | Current reading. The browser clamps it into [min, max].MDN<meter value> |
min | number | 0 | Lower bound of the range. |
max | number | 1 | Upper bound of the range. Default 1, so value=0.75 reads as 75%. |
low | number | — | Upper bound of the low zone. Values below it render in the low colour band.MDN<meter low> |
high | number | — | Lower bound of the high zone. Values above it render in the high colour band.MDN<meter high> |
optimum | number | — | The preferred value. Tells the browser which zone is "good" (green), making the others suboptimal (amber) or far-off (red).MDN<meter optimum> |
valuetext | string | — | Human-readable value for AT (e.g. "12.4 GB of 16 GB"). Sets aria-valuetext. Per APG, use it when a bare percentage isn't user-friendly.APGMeter pattern |
children | Child | — | Fallback text shown by legacy AT and browsers without <meter> support. Defaults to a "value / max" string. |
ariaLabel | string | — | Accessible name when no visible <label>.MDNaria-label |
ariaLabelledby | string | — | Id of a visible element providing the accessible name.MDNaria-labelledby |
ariaDescribedby | string | — | Id of an element describing this control (announced after the name).MDNaria-describedby |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
class | string | — | Extra Tailwind classes appended to the root element. |
* required