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.
Installation
npx @glasskit-ui/cli add progressInstall 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
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value (clamped to [0, max]). |
max | number | 100 | Total / step count. |
variant | "linear" | "step" | "linear" | Continuous bar or step-of-N dots. |
label | ReactNode | — | Optional 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
stepvariant). 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
onCompletefires at zero.
Confirm
A decision screen: a prompt plus a two-button action bar. Drop it into a Screen stage; useDpad seeds focus on the primary action.
AsyncView
The four-state async renderer every data screen needs: placeholder → loading → success / error. You own the async work and pass the status; AsyncView picks the view, with lens-ready defaults.