Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Progress

The native `<progress>`, determinate or indeterminate.

Installation

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

Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

Default

62%
<div data-ui="field">
  <label
    style="
      --display: flex;
      --items: center;
      --justify: space-between;
    "
    data-field-slot="label"
    for="f-upload"
  >
    <span>Reticulating splines…</span>
    <span
      style="
        --font-size: var(--font-size-xs);
        --text: var(--color-muted-foreground);
      "
      >62%</span
    >
  </label>
  <progress data-ui="progress" id="f-upload" max="100" value="62">62%</progress>
</div>
/**
 * progress.css — Progress ([data-ui~="progress"] on native <progress>)
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved)
 * @uses       appearance: none — element box becomes the track; only the fill
 *             needs vendor pseudo-elements (::-webkit-progress-value,
 *             ::-moz-progress-bar) — never combine the two vendors in one
 *             selector list (an unknown pseudo invalidates the whole rule)
 * @uses       :indeterminate — animated stripe on the element background
 * @tokens     --ui-progress-* (@layer variables)
 * @consumedby meter.css
 */

@layer variables {
  :root {
    --ui-progress-w: 100%;
    --ui-progress-h: calc(var(--spacing) * 1.5);
    --ui-progress-rounded: var(--radius-full);
    --ui-progress-bg: var(--color-muted);
    --ui-progress-track-border: 1px solid transparent;
    --ui-progress-bar-bg: var(--color-primary);
    --ui-progress-indeterminate-duration: 1s;
  }
}

@layer reset {
  /* ===========================================================================
  PROGRESS — native <progress> fill redraw
  The element box is the track (overflow clips the fill's corners); only the
  fill pseudos are redrawn, one rule per vendor.
  =========================================================================== */

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

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

  :where([data-ui~="progress"])::-webkit-progress-value {
    background-color: var(--ui-progress-bar-bg);
    border-radius: inherit;
    transition: inline-size var(--default-transition-duration)
      var(--default-transition-timing-function);
  }

  :where([data-ui~="progress"])::-moz-progress-bar {
    background-color: var(--ui-progress-bar-bg);
    border-radius: inherit;
  }

  /* Stripe on the element itself so it renders the same in every engine; the
     vendor fills go transparent to avoid a phantom bar. Percentage stops in a
     square tile wrap seamlessly at 45°, and animating exactly one tile keeps
     the loop jump-free; the padding-box clip keeps stripes out of the border. */
  :where([data-ui~="progress"]:indeterminate) {
    --_stripe: color-mix(in oklch, var(--ui-progress-bar-bg) 30%, transparent);
    background-image: linear-gradient(
      -45deg,
      var(--_stripe) 25%,
      transparent 25% 50%,
      var(--_stripe) 50% 75%,
      transparent 75%
    );
    background-size: calc(var(--spacing) * 4) calc(var(--spacing) * 4);
    background-origin: padding-box;
    background-clip: padding-box;
  }

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

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

  @media (prefers-reduced-motion: no-preference) {
    :where([data-ui~="progress"]:indeterminate) {
      animation: ui-progress-slide var(--ui-progress-indeterminate-duration) linear infinite;
    }
  }

  @keyframes ui-progress-slide {
    from {
      background-position: 0 0;
    }

    to {
      background-position: calc(var(--spacing) * 4) 0;
    }
  }
}

Indeterminate

<progress data-ui="progress" aria-label="Loading"></progress>
/**
 * progress.css — Progress ([data-ui~="progress"] on native <progress>)
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved)
 * @uses       appearance: none — element box becomes the track; only the fill
 *             needs vendor pseudo-elements (::-webkit-progress-value,
 *             ::-moz-progress-bar) — never combine the two vendors in one
 *             selector list (an unknown pseudo invalidates the whole rule)
 * @uses       :indeterminate — animated stripe on the element background
 * @tokens     --ui-progress-* (@layer variables)
 * @consumedby meter.css
 */

@layer variables {
  :root {
    --ui-progress-w: 100%;
    --ui-progress-h: calc(var(--spacing) * 1.5);
    --ui-progress-rounded: var(--radius-full);
    --ui-progress-bg: var(--color-muted);
    --ui-progress-track-border: 1px solid transparent;
    --ui-progress-bar-bg: var(--color-primary);
    --ui-progress-indeterminate-duration: 1s;
  }
}

@layer reset {
  /* ===========================================================================
  PROGRESS — native <progress> fill redraw
  The element box is the track (overflow clips the fill's corners); only the
  fill pseudos are redrawn, one rule per vendor.
  =========================================================================== */

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

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

  :where([data-ui~="progress"])::-webkit-progress-value {
    background-color: var(--ui-progress-bar-bg);
    border-radius: inherit;
    transition: inline-size var(--default-transition-duration)
      var(--default-transition-timing-function);
  }

  :where([data-ui~="progress"])::-moz-progress-bar {
    background-color: var(--ui-progress-bar-bg);
    border-radius: inherit;
  }

  /* Stripe on the element itself so it renders the same in every engine; the
     vendor fills go transparent to avoid a phantom bar. Percentage stops in a
     square tile wrap seamlessly at 45°, and animating exactly one tile keeps
     the loop jump-free; the padding-box clip keeps stripes out of the border. */
  :where([data-ui~="progress"]:indeterminate) {
    --_stripe: color-mix(in oklch, var(--ui-progress-bar-bg) 30%, transparent);
    background-image: linear-gradient(
      -45deg,
      var(--_stripe) 25%,
      transparent 25% 50%,
      var(--_stripe) 50% 75%,
      transparent 75%
    );
    background-size: calc(var(--spacing) * 4) calc(var(--spacing) * 4);
    background-origin: padding-box;
    background-clip: padding-box;
  }

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

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

  @media (prefers-reduced-motion: no-preference) {
    :where([data-ui~="progress"]:indeterminate) {
      animation: ui-progress-slide var(--ui-progress-indeterminate-duration) linear infinite;
    }
  }

  @keyframes ui-progress-slide {
    from {
      background-position: 0 0;
    }

    to {
      background-position: calc(var(--spacing) * 4) 0;
    }
  }
}

Attributes

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

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.

progress hooks
HookDefaultState
--ui-progress-bar-bgvar(--color-primary)—
--ui-progress-bgvar(--color-muted)—
--ui-progress-hcalc(var(--spacing) * 1.5)—
--ui-progress-indeterminate-duration1s—
--ui-progress-roundedvar(--radius-full)—
--ui-progress-track-border1px solid transparent—
--ui-progress-w100%—