GlassKit UI
Primitives

Progress

Emitted progress in two shapes: a continuous linear bar (a native <progress>, so the fill needs no inline style; also covers countdowns) and discrete step-of-N dots for wizards.

64%
Linear + step, same component

Installation

npx @glasskit-ui/cli add progress

Install the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:

npm install @glasskit-ui/react
// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string {  return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined {  return typeof label === "string" ? label : undefined;}
// components/glasskit/progress.tsximport type { ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";/** * <Progress> — emitted progress, two shapes: *   - "linear"  a continuous bar (also covers countdown: feed a decreasing *               value and a time label). Uses a native <progress>, so the *               dynamic fill needs no inline style. *   - "step"    discrete step-of-N dots (wizard / pinch-advance). * * `value` is clamped to [0, max]. For "step", value = completed steps. */export function Progress({  value,  max = 100,  variant = "linear",  label,  className,}: {  value: number;  max?: number;  variant?: "linear" | "step";  /** Optional caption shown with the bar (e.g. "3 of 5", "0:42 left"). */  label?: ReactNode;  className?: string;}) {  const clamped = Math.max(0, Math.min(value, max));  if (variant === "step") {    // Steps must be a sane integer count — a fractional or negative max    // would make Array.from misrender the dots.    const steps = Math.max(0, Math.floor(max));    return (      <div        className={cn("flex items-center gap-2", className)}        role="progressbar"        aria-valuenow={clamped}        aria-valuemin={0}        aria-valuemax={steps}        aria-label={stringLabel(label)}      >        {Array.from({ length: steps }, (_, i) => (          <span            key={i}            className={cn(              "size-3 rounded-full transition-[background,box-shadow] duration-[250ms] ease-in-out",              i < clamped                ? "bg-primary [box-shadow:0_0_7px_color-mix(in_oklab,var(--accent)_45%,transparent)]"                : "bg-[rgba(255,255,255,0.16)]",            )}          />        ))}      </div>    );  }  return (    <div className={cn("flex w-full flex-col gap-2", className)}>      <progress        className="gk-progress__el"        value={clamped}        max={max}        aria-label={stringLabel(label)}      />      {label != null ? (        <div className="t-caption flex items-center justify-between text-foreground-faint [font-variant-numeric:tabular-nums]">          {label}        </div>      ) : null}    </div>  );}

Usage

<Progress value={64} label="Downloading · 64%" /><Progress variant="step" value={2} max={4} />

Props

PropTypeDefaultDescription
valuenumberCurrent value (clamped to [0, max]).
maxnumber100Total / step count.
variant"linear" | "step""linear"Continuous bar or step-of-N dots.
labelReactNodeOptional caption.

When to use

Progress, Meter, and Timer all draw a quantity; they answer different questions:

  • Progress is task completion: a download, an upload, step n of m (the step variant). Always moving toward done.
  • Meter is a level that just is: battery, volume, signal. No notion of completion; it reads as a gauge.
  • Timer is time remaining: a big tabular countdown with an optional drain bar. Self-ticking, and onComplete fires at zero.

On this page