Feedback

Meter

A gauge for a measurement within a known range, such as storage used or a score, toned by thresholds.

Built on
role="meter"Intl.NumberFormat
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
2.0 kB
Dependencies
React
Storage82 GB of 100 GB, nearly full
Try its options

Loading its options.

Code
import { Meter } from "@/components/equip/meter";

export default function Demo() {
  return (
    <Meter
      label="Storage"
      value={82}
      max={100}
      low={70}
      high={90}
      optimum={0}
      status="nearly full"
      format={{ style: "unit", unit: "gigabyte" }}
      style={{ width: 224 }}
    />
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div role="meter" aria-label="Storage" aria-valuenow="82" aria-valuemin="0" aria-valuemax="100" aria-valuetext="82 GB of 100 GB, nearly full" class="eq-meter" data-tone="warn" style="width:224px;--_v:0.82">
  <span class="eq-meter-label">Storage</span>
  <span class="eq-meter-value">82 GB of 100 GB, nearly full</span>
  <span class="eq-meter-track">
    <span class="eq-meter-fill"></span>
  </span>
</div>

Examples

Regions

With optimum below low, low values are ok, the middle band warns and anything past high is danger, as on the native meter.

Build cache, web240 MB of 1,000 MB
Build cache, api720 MB of 1,000 MB, low
Build cache, worker968 MB of 1,000 MB, evicting old builds
Code
import { Meter } from "@/components/equip/meter";

export default function Example() {
  const quota = { max: 1000, low: 600, high: 900, optimum: 0, format: { style: "unit", unit: "megabyte" } } as const;
  return (
    <div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
      <Meter label="Build cache, web" value={240} {...quota} />
      <Meter label="Build cache, api" value={720} {...quota} />
      <Meter label="Build cache, worker" value={968} {...quota} status="evicting old builds" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
  <div role="meter" aria-label="Build cache, web" aria-valuenow="240" aria-valuemin="0" aria-valuemax="1000" aria-valuetext="240 MB of 1,000 MB" class="eq-meter" data-tone="ok" style="--_v:0.24">
    <span class="eq-meter-label">Build cache, web</span>
    <span class="eq-meter-value">240 MB of 1,000 MB</span>
    <span class="eq-meter-track">
      <span class="eq-meter-fill"></span>
    </span>
  </div>
  <div role="meter" aria-label="Build cache, api" aria-valuenow="720" aria-valuemin="0" aria-valuemax="1000" aria-valuetext="720 MB of 1,000 MB, low" class="eq-meter" data-tone="warn" style="--_v:0.72">
    <span class="eq-meter-label">Build cache, api</span>
    <span class="eq-meter-value">720 MB of 1,000 MB, low</span>
    <span class="eq-meter-track">
      <span class="eq-meter-fill"></span>
    </span>
  </div>
  <div role="meter" aria-label="Build cache, worker" aria-valuenow="968" aria-valuemin="0" aria-valuemax="1000" aria-valuetext="968 MB of 1,000 MB, evicting old builds" class="eq-meter" data-tone="danger" style="--_v:0.968">
    <span class="eq-meter-label">Build cache, worker</span>
    <span class="eq-meter-value">968 MB of 1,000 MB, evicting old builds</span>
    <span class="eq-meter-track">
      <span class="eq-meter-fill"></span>
    </span>
  </div>
</div>

Segments

segments draws discrete blocks, which suits scores and small counts; the value is rounded to the nearest block.

Password strength3 of 5, fair
Seats used7 of 10
Code
import { Meter } from "@/components/equip/meter";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 16, maxWidth: 280 }}>
      <Meter label="Password strength" value={3} max={5} low={2} high={4} optimum={5} segments={5} status="fair" />
      <Meter label="Seats used" value={7} max={10} segments={10} />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:280px">
  <div role="meter" aria-label="Password strength" aria-valuenow="3" aria-valuemin="0" aria-valuemax="5" aria-valuetext="3 of 5, fair" class="eq-meter" data-tone="warn" style="--_v:0.6">
    <span class="eq-meter-label">Password strength</span>
    <span class="eq-meter-value">3 of 5, fair</span>
    <span class="eq-meter-blocks" style="--_n:5">
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block"></span>
      <span class="eq-meter-block"></span>
    </span>
  </div>
  <div role="meter" aria-label="Seats used" aria-valuenow="7" aria-valuemin="0" aria-valuemax="10" aria-valuetext="7 of 10" class="eq-meter" data-tone="neutral" style="--_v:0.7">
    <span class="eq-meter-label">Seats used</span>
    <span class="eq-meter-value">7 of 10</span>
    <span class="eq-meter-blocks" style="--_n:10">
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block" data-on="true"></span>
      <span class="eq-meter-block"></span>
      <span class="eq-meter-block"></span>
      <span class="eq-meter-block"></span>
    </span>
  </div>
</div>

A band in the middle

With optimum between low and high, both ends warn; a range that does not start at zero is read with its bounds.

Server room temperature23°C, range 15°C to 35°C
Cold aisle temperature16°C, range 15°C to 35°C, too cold
Code
import { Meter } from "@/components/equip/meter";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
      <Meter label="Server room temperature" value={23} min={15} max={35} low={18} high={26} optimum={21} format={{ style: "unit", unit: "celsius" }} />
      <Meter label="Cold aisle temperature" value={16} min={15} max={35} low={18} high={26} optimum={21} format={{ style: "unit", unit: "celsius" }} status="too cold" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
  <div role="meter" aria-label="Server room temperature" aria-valuenow="23" aria-valuemin="15" aria-valuemax="35" aria-valuetext="23°C, range 15°C to 35°C" class="eq-meter" data-tone="ok" style="--_v:0.4">
    <span class="eq-meter-label">Server room temperature</span>
    <span class="eq-meter-value">23°C, range 15°C to 35°C</span>
    <span class="eq-meter-track">
      <span class="eq-meter-fill"></span>
    </span>
  </div>
  <div role="meter" aria-label="Cold aisle temperature" aria-valuenow="16" aria-valuemin="15" aria-valuemax="35" aria-valuetext="16°C, range 15°C to 35°C, too cold" class="eq-meter" data-tone="warn" style="--_v:0.05">
    <span class="eq-meter-label">Cold aisle temperature</span>
    <span class="eq-meter-value">16°C, range 15°C to 35°C, too cold</span>
    <span class="eq-meter-track">
      <span class="eq-meter-fill"></span>
    </span>
  </div>
</div>

Props

PropTypeDefaultWhat it does
labelstringnoneWhat is measured. Shown above the bar and used as the accessible name.
valuenumbernoneThe measurement. Clamped to min and max.
minnumber0Bottom of the range.
maxnumber100Top of the range.
lownumbernoneBelow this the value counts as low, as on the native meter.
highnumbernoneAbove this the value counts as high.
optimumnumbernoneThe best value. Where it sits against low and high decides which region is ok, warn or danger.
statusstringnoneWords for the current state, such as "nearly full". Defaults to low, high, very low or very high outside the best region.
formatIntl.NumberFormatOptionsnoneHow the value and range are written, such as a unit.
localestring"en-US"Passed to Intl.NumberFormat.
showValuebooleantrueShows the value text opposite the label.
segmentsnumbernoneDraws the bar as this many discrete blocks.
...restComponentProps<"div">noneEvery native div attribute, on the meter element.

Anatomy

root.eq-meterrole="meter" with value, range and value text; data-tone is neutral, ok, warn or danger.
label.eq-meter-label
value.eq-meter-valueThe same words as aria-valuetext.
track.eq-meter-track
fill.eq-meter-fillIts width eases to the value.
blocks.eq-meter-blocksReplaces the track when segments is set.
block.eq-meter-blockdata-on when it is filled.

Notes

  • Follows the native meter's regions: with optimum below low, low values are ok, values up to high warn and values past high are danger, and the reverse.
  • Without low, high or optimum the bar is neutral ink, since there is nothing to judge it against.
  • The state is said in words in the value text, such as "82 GB of 100 GB, nearly full", so the tone is never the only cue.
  • Use Progress for how far a task has got; a meter is for a measurement that can go up or down.
  • Renders on the server with no client JavaScript.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/meter.tsx97 lines
import type { CSSProperties, ComponentProps } from "react";
import "./meter.css";

export type MeterProps = ComponentProps<"div"> & {
  /** What is measured, such as "Storage". Shown above the bar and used as the accessible name. */
  label: string;
  value: number;
  min?: number;
  max?: number;
  /** Below this the value counts as low. Same meaning as on the native meter element. */
  low?: number;
  /** Above this the value counts as high. */
  high?: number;
  /** The best value. Where it sits against low and high decides which region is ok, warn or danger. */
  optimum?: number;
  /** Words for the current state, such as "nearly full". Defaults to "low", "high", "very low" or "very high". */
  status?: string;
  format?: Intl.NumberFormatOptions;
  locale?: string;
  /** Shows the value text opposite the label. It is always the accessible value text. */
  showValue?: boolean;
  /** Draws the bar as this many discrete blocks instead of one fill. */
  segments?: number;
};

type Region = "optimum" | "suboptimal" | "worst";

export function Meter({
  label,
  value,
  min = 0,
  max = 100,
  low,
  high,
  optimum,
  status,
  format,
  locale = "en-US",
  showValue = true,
  segments,
  className,
  style,
  ...rest
}: MeterProps) {
  const top = max > min ? max : min + 1;
  const clamp = (n: number, a: number, b: number) => Math.min(b, Math.max(a, n));
  const v = clamp(Number.isFinite(value) ? value : min, min, top);
  const lowB = clamp(low ?? min, min, top);
  const highB = clamp(high ?? top, lowB, top);
  const best = clamp(optimum ?? (min + top) / 2, min, top);

  let region: Region;
  if (best < lowB) region = v <= lowB ? "optimum" : v <= highB ? "suboptimal" : "worst";
  else if (best > highB) region = v >= highB ? "optimum" : v >= lowB ? "suboptimal" : "worst";
  else region = v >= lowB && v <= highB ? "optimum" : "suboptimal";

  const judged = low !== undefined || high !== undefined || optimum !== undefined;
  const tone = !judged ? "neutral" : region === "optimum" ? "ok" : region === "suboptimal" ? "warn" : "danger";
  const side = v > highB ? "high" : "low";
  const said = status ?? (judged && region !== "optimum" ? (region === "worst" ? `very ${side}` : side) : undefined);

  const number = new Intl.NumberFormat(locale, format);
  const amount = min === 0 ? `${number.format(v)} of ${number.format(top)}` : `${number.format(v)}, range ${number.format(min)} to ${number.format(top)}`;
  const text = said ? `${amount}, ${said}` : amount;
  const fraction = (v - min) / (top - min);
  const count = segments && segments > 0 ? Math.floor(segments) : 0;
  const lit = Math.round(fraction * count);

  return (
    <div
      role="meter"
      aria-label={label}
      aria-valuenow={v}
      aria-valuemin={min}
      aria-valuemax={top}
      aria-valuetext={text}
      {...rest}
      className={["eq-meter", className].filter(Boolean).join(" ")}
      data-tone={tone}
      style={{ ...style, "--_v": fraction } as CSSProperties}
    >
      <span className="eq-meter-label">{label}</span>
      {showValue && <span className="eq-meter-value">{text}</span>}
      {count > 0 ? (
        <span className="eq-meter-blocks" style={{ "--_n": count } as CSSProperties}>
          {Array.from({ length: count }, (_, i) => (
            <span key={i} className="eq-meter-block" data-on={i < lit || undefined} />
          ))}
        </span>
      ) : (
        <span className="eq-meter-track">
          <span className="eq-meter-fill" />
        </span>
      )}
    </div>
  );
}
components/equip/meter.css84 lines
@layer equip {
  .eq-meter {
    --_fill: var(--eq-solid);

    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    gap: var(--eq-space-1) var(--eq-space-3);
    font: var(--eq-weight) var(--eq-text-s) / 1.4 var(--eq-font);
  }

  .eq-meter[data-tone="ok"] {
    --_fill: var(--eq-ok);
  }

  .eq-meter[data-tone="warn"] {
    --_fill: var(--eq-warn);
  }

  .eq-meter[data-tone="danger"] {
    --_fill: var(--eq-danger);
  }

  .eq-meter-label {
    grid-area: 1 / 1;
    min-width: 0;
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
  }

  .eq-meter-value {
    grid-area: 1 / 2;
    color: var(--eq-ink-2);
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  .eq-meter[data-tone="warn"] .eq-meter-value,
  .eq-meter[data-tone="danger"] .eq-meter-value {
    color: var(--_fill);
  }

  .eq-meter-track,
  .eq-meter-blocks {
    grid-area: 2 / 1 / 3 / 3;
    height: 6px;
  }

  .eq-meter-track {
    display: block;
    border-radius: var(--eq-radius-full);
    overflow: hidden;
    background: var(--eq-tint-strong);
  }

  /* A measurement: it travels to the new value but never overshoots it. */
  .eq-meter-fill {
    display: block;
    width: calc(var(--_v) * 100%);
    height: 100%;
    border-radius: inherit;
    background: var(--_fill);
    transition:
      width var(--eq-dur-move) var(--eq-ease),
      background-color var(--eq-dur) var(--eq-ease);
  }

  .eq-meter-blocks {
    display: grid;
    grid-template-columns: repeat(var(--_n), minmax(0, 1fr));
    gap: 3px;
    height: 8px;
  }

  .eq-meter-block {
    border-radius: 2px;
    background: var(--eq-tint-strong);
    transition: background-color var(--eq-dur) var(--eq-ease);
  }

  .eq-meter-block[data-on] {
    background: var(--_fill);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index