Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Meter

The native `<meter>`, restyled.

Installation

pnpm dlx zazz-ui add meter
npx zazz-ui add meter
yarn dlx zazz-ui add meter
bunx zazz-ui add meter

Also adds progress, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

52 GB of 100 GB
93 GB of 100 GB
<div
  style="
    --display: flex-col;
    --gap: 4;
    --w: 100%;
    --max-w: 96;
  "
>
  <div data-ui="field">
    <label
      style="
        --display: flex;
        --items: center;
        --justify: space-between;
      "
      data-field-slot="label"
      for="f-storage"
    >
      <span>Storage used</span>
      <span
        style="
          --font-size: var(--font-size-xs);
          --text: var(--color-muted-foreground);
        "
        >52/100 GB</span
      >
    </label>
    <meter
      data-ui="meter"
      id="f-storage"
      min="0"
      max="100"
      low="60"
      high="85"
      optimum="30"
      value="52"
    >
      52 GB of 100 GB
    </meter>
  </div>
  <div data-ui="field">
    <label
      style="
        --display: flex;
        --items: center;
        --justify: space-between;
      "
      data-field-slot="label"
      for="f-storage-high"
    >
      <span>Nearly full</span>
      <span
        style="
          --font-size: var(--font-size-xs);
          --text: var(--color-muted-foreground);
        "
        >93/100 GB</span
      >
    </label>
    <meter
      data-ui="meter"
      id="f-storage-high"
      min="0"
      max="100"
      low="60"
      high="85"
      optimum="30"
      value="93"
    >
      93 GB of 100 GB
    </meter>
  </div>
</div>
/**
 * meter.css: Meter ([data-ui~="meter"] on native <meter>)
 *
 * @layer      variables, reset
 * @requires   _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved), progress.css
 * @uses       appearance: none: element box becomes the track; only the fill
 *             needs vendor pseudo-elements (::-webkit-meter-*-value,
 *             ::-moz-meter-bar + :-moz-meter-* state classes); never combine
 *             the two vendors in one selector list
 * @uses       attr(<attr> type(<number>)): where typed attr() exists
 *             (Chromium 133+) the fill is painted on the element from the
 *             meter's own attributes, because Blink locks the shadow parts'
 *             geometry (backgrounds apply, heights do not); @supports gated
 * @uses       Engines without typed attr() keep the vendor-pseudo fill;
 *             recorded degradation in Blink pre-133: themed track + thin
 *             centered fill (browser policy exception)
 * @tokens     --ui-meter-* (@layer variables); geometry aliases --ui-progress-*
 */

@layer variables {
  :root {
    /* Geometry rides the progress family so the two bars read identically;
       retune a --ui-progress-* token and both follow. */
    --ui-meter-w: var(--ui-progress-w);
    --ui-meter-h: var(--ui-progress-h);
    --ui-meter-rounded: var(--ui-progress-rounded);
    --ui-meter-bg: var(--ui-progress-bg);
    --ui-meter-track-border: var(--ui-progress-track-border);

    /* Fills are semantic: a meter communicates good/bad, not completion */
    --ui-meter-bar-bg: var(--color-success);
    --ui-meter-bar-bg--suboptimum: var(--color-warning);
    --ui-meter-bar-bg--even-less-good: var(--color-destructive);
  }
}

@layer reset {
  /* ===========================================================================
  METER: native <meter> fill redraw
  - Same track-on-the-element strategy as progress.css. WebKit exposes one
    pseudo per state; Firefox exposes one fill pseudo colored by :-moz-meter-*
    state classes. One rule per vendor pseudo, by necessity.
  =========================================================================== */

  :where([data-ui~="meter"]) {
    --_w-len: var(--_w-resolved, var(--ui-meter-w));
    --_w-num: var(--_w-resolved, var(--ui-meter-w));
    inline-size: calc(var(--_w-len) + var(--_w-num) * var(--spacing));
    --_h-len: var(--_h-resolved, var(--ui-meter-h));
    --_h-num: var(--_h-resolved, var(--ui-meter-h));
    block-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
    overflow: hidden;
    background-color: var(--_bg-resolved, var(--ui-meter-bg));
    border: var(--ui-meter-track-border);
    border-radius: var(--_rounded-resolved, var(--ui-meter-rounded));
  }

  :where([data-ui~="meter"])::-webkit-meter-bar {
    background: transparent;
    border: none;
    block-size: 100%;
  }

  /* The value pseudos keep a UA-default height unless given one explicitly */
  :where([data-ui~="meter"])::-webkit-meter-optimum-value {
    block-size: 100%;
    background: var(--ui-meter-bar-bg);
    border-radius: inherit;
  }

  :where([data-ui~="meter"])::-webkit-meter-suboptimum-value {
    block-size: 100%;
    background: var(--ui-meter-bar-bg--suboptimum);
    border-radius: inherit;
  }

  :where([data-ui~="meter"])::-webkit-meter-even-less-good-value {
    block-size: 100%;
    background: var(--ui-meter-bar-bg--even-less-good);
    border-radius: inherit;
  }

  :where([data-ui~="meter"])::-moz-meter-bar {
    block-size: 100%;
    border-radius: inherit;
  }

  :where([data-ui~="meter"]:-moz-meter-optimum)::-moz-meter-bar {
    background: var(--ui-meter-bar-bg);
  }

  :where([data-ui~="meter"]:-moz-meter-sub-optimum)::-moz-meter-bar {
    background: var(--ui-meter-bar-bg--suboptimum);
  }

  :where([data-ui~="meter"]:-moz-meter-sub-sub-optimum)::-moz-meter-bar {
    background: var(--ui-meter-bar-bg--even-less-good);
  }

  /* ===========================================================================
  ATTRIBUTE-PAINTED FILL: Blink accepts backgrounds on the meter shadow parts
  but ignores their geometry entirely (author heights, even !important, never
  apply), leaving a thin centered fill. Where typed attr() exists, paint the
  fill on the element itself instead: a hard-stop gradient sized from the
  meter's own attributes, reproducing the HTML spec's region algorithm:
  value and optimum each land in a low/between/high region, and the distance
  between the two regions (0/1/2) picks optimal / suboptimal / even-less-good.
  The shadow fills are blanked underneath so nothing double-paints.
  =========================================================================== */

  @supports (inline-size: calc(attr(value type(<number>), 0) * 1px)) {
    :where([data-ui~="meter"]) {
      /* Attribute facts, with the spec's defaults */
      --_val: attr(value type(<number>), 0);
      --_min: attr(min type(<number>), 0);
      --_max: attr(max type(<number>), 1);
      --_low: attr(low type(<number>), var(--_min));
      --_high: attr(high type(<number>), var(--_max));
      --_opt: attr(optimum type(<number>), calc((var(--_min) + var(--_max)) / 2));

      --_pct: calc((var(--_val) - var(--_min)) / (var(--_max) - var(--_min)) * 100%);

      /* Region index 0/1/2 = below low / between / above high. The ×100000
         saturation stands in for sign(); the +1 makes the low bound inclusive.
         Assumes attribute deltas of at least 0.00001. */
      --_region-val: calc(
        clamp(0, (var(--_val) - var(--_low)) * 100000 + 1, 1) +
          clamp(0, (var(--_val) - var(--_high)) * 100000, 1)
      );
      --_region-opt: calc(
        clamp(0, (var(--_opt) - var(--_low)) * 100000 + 1, 1) +
          clamp(0, (var(--_opt) - var(--_high)) * 100000, 1)
      );
      --_distance: max(
        var(--_region-val) - var(--_region-opt),
        var(--_region-opt) - var(--_region-val)
      );

      /* distance 0 → optimal, 1 → suboptimal, 2 → even less good */
      --_fill: color-mix(
        in oklch,
        var(--ui-meter-bar-bg--even-less-good) calc(clamp(0, var(--_distance) - 1, 1) * 100%),
        color-mix(
          in oklch,
          var(--ui-meter-bar-bg--suboptimum) calc(min(var(--_distance), 1) * 100%),
          var(--ui-meter-bar-bg)
        )
      );

      background-image: linear-gradient(
        to right,
        var(--_fill) var(--_pct),
        transparent var(--_pct)
      );
      background-origin: padding-box;
      background-clip: padding-box;
    }

    :where([data-ui~="meter"]:dir(rtl)) {
      background-image: linear-gradient(to left, var(--_fill) var(--_pct), transparent var(--_pct));
    }

    /* The shadow fills would double-paint under the gradient (the UA styles
       these parts with a bevel background-image) */
    :where([data-ui~="meter"])::-webkit-meter-optimum-value {
      background: transparent;
    }

    :where([data-ui~="meter"])::-webkit-meter-suboptimum-value {
      background: transparent;
    }

    :where([data-ui~="meter"])::-webkit-meter-even-less-good-value {
      background: transparent;
    }

    :where([data-ui~="meter"])::-moz-meter-bar {
      background: transparent;
    }
  }
}

Attributes

meter attributes
AttributeKindValuesNotes
data-ui="meter"Identityfree valueIdentity token in data-ui.

Requires: progress.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

meter hooks
HookDefaultState
--ui-meter-bar-bgvar(--color-success)—
--ui-meter-bar-bg--even-less-goodvar(--color-destructive)even-less-good
--ui-meter-bar-bg--suboptimumvar(--color-warning)suboptimum
--ui-meter-bgvar(--ui-progress-bg)—
--ui-meter-hvar(--ui-progress-h)—
--ui-meter-roundedvar(--ui-progress-rounded)—
--ui-meter-track-bordervar(--ui-progress-track-border)—
--ui-meter-wvar(--ui-progress-w)—