Meter
A bounded ring gauge for a level (battery, signal, effort), distinct from Progress, which tracks task completion. The arc fills via an SVG stroke-dashoffset; value is clamped to [0, max].
Installation
npx @glasskit-ui/cli add meterInstall 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/meter.tsximport type { ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";const R = 42;const CIRC = 2 * Math.PI * R;/** * <Meter> — a bounded ring gauge for a level (battery, signal, intensity), as * opposed to <Progress> which tracks task completion. The fill is revealed with * an SVG `stroke-dashoffset` *attribute* (not inline style); `value` is clamped * to [0, max]. */export function Meter({ value, max = 100, label, unit, className,}: { value: number; max?: number; label?: ReactNode; unit?: ReactNode; className?: string;}) { const clamped = Math.max(0, Math.min(value, max)); const pct = max > 0 ? clamped / max : 0; const offset = CIRC * (1 - pct); return ( <div className={cn( "relative inline-flex size-[168px] items-center justify-center", className, )} role="meter" aria-valuenow={clamped} aria-valuemin={0} aria-valuemax={max} aria-label={stringLabel(label)} > <svg viewBox="0 0 100 100" className="size-full"> <circle cx="50" cy="50" r={R} className="[fill:none] [stroke-width:7] [stroke:rgba(255,255,255,0.1)]" /> <circle cx="50" cy="50" r={R} className="[fill:none] [stroke-width:7] [stroke:var(--accent)] [stroke-linecap:round] [filter:drop-shadow(0_0_6px_color-mix(in_oklab,var(--accent)_45%,transparent))] [transition:stroke-dashoffset_0.4s_ease]" strokeDasharray={CIRC} strokeDashoffset={offset} transform="rotate(-90 50 50)" /> </svg> <span className="absolute flex flex-col items-center gap-0.5"> <span className="t-readout"> {value} {unit != null ? ( <span className="text-[13px] text-foreground-faint">{unit}</span> ) : null} </span> {label != null ? ( <span className="t-caption uppercase tracking-[0.14em] text-foreground-faint"> {label} </span> ) : null} </span> </div> );}Usage
<Meter value={72} max={100} label="Effort" unit="%" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current level (clamped to [0, max]). |
max | number | 100 | Upper bound. |
label | ReactNode | — | Caption under the value. |
unit | ReactNode | — | Trailing unit. |
When to use
Meter shows a level with no endpoint (battery, volume, signal). For task completion use Progress, and for time remaining use Timer.
Toaster
The toast / notification system, Sonner (Emil Kowalski's library) themed to the lens. Mount <Toaster> once, then fire imperatively with toast(); it handles the queue, stacking, auto-dismiss, and enter/exit motion. Top-anchored, so Screen's Cue line owns the bottom strip.
StatGrid
A compact grid of readouts for a multi-metric glance (a complication cluster). Pure display, tabular numerals. Keep it to 2 to 4 cells.