GlassKit UI
Components

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

600 × 600 · live

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.tsexport type ClassValue = string | number | null | undefined | false;/** * Join truthy class names. Dependency-free on purpose: the lens components * style via bespoke semantic classes (no conflicting Tailwind utilities to * de-dupe), so this needs no clsx/tailwind-merge and resolves from anywhere * the registry is vendored. */export function cn(...inputs: ClassValue[]): string {  return inputs.filter(Boolean).join(" ");}/** * 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("gk-steps", 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("gk-step", i < clamped && "gk-step--on")}          />        ))}      </div>    );  }  return (    <div className={cn("gk-progress", className)}>      <progress        className="gk-progress__el"        value={clamped}        max={max}        aria-label={stringLabel(label)}      />      {label != null ? (        <div className="gk-progress__meta t-caption">{label}</div>      ) : null}    </div>  );}

Usage

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

Props

Prop

Type