Primitives
Slider
A continuous level control (volume, brightness, the quick controls). A native range tinted with accent-color; arrow keys / Neural-Band pinch-twist adjust it. Controlled via value + onChange.
Installation
npx @glasskit-ui/cli add sliderInstall 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/slider.tsximport type { ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";/** * <Slider> — a continuous level control (volume, brightness — the quick * controls). A native range so the fill + thumb need no inline style * (`accent-color` tints them); arrow keys / Neural-Band pinch-twist adjust it. * Controlled via `value` + `onChange`. Omit `onChange` for a read-only * display — the slider then leaves the D-pad focus order (`readOnly` is * meaningless on a range input; an adjustable-looking dead control is worse * than a plain level display). */export function Slider({ value, min = 0, max = 100, label, icon, onChange, className,}: { value: number; min?: number; max?: number; /** a11y / caption label. */ label?: ReactNode; /** Leading glyph — typically a <Icon> (volume / brightness). */ icon?: ReactNode; onChange?: (next: number) => void; className?: string;}) { return ( <div className={cn("flex w-full items-center gap-4", className)}> {/* gk-slider__icon stays: it sizes/tints the Icon child via a * descendant rule (.gk-slider__icon .gk-icon). */} {icon != null ? <span className="gk-slider__icon">{icon}</span> : null} {/* gk-slider__input stays: the range track + thumb are styled through * ::-webkit-slider-thumb / ::-moz-range-thumb pseudo-elements + the focus * ring — none of which Tailwind utilities can reach. */} <input type="range" className={cn("gk-slider__input", onChange != null && "focusable")} min={min} max={max} value={value} // readOnly only suppresses React's controlled-input warning (the // attribute itself is meaningless on type="range") — the real // read-only mechanism is leaving the focus order above. readOnly={onChange == null} tabIndex={onChange == null ? -1 : undefined} aria-readonly={onChange == null ? true : undefined} aria-label={stringLabel(label)} onChange={ onChange ? (e) => onChange(Number(e.currentTarget.value)) : undefined } /> {label != null ? ( <span className="t-caption flex-none text-foreground-faint"> {label} </span> ) : null} </div> );}Usage
<Slider value={volume} onChange={setVolume} icon={<Icon size="md"><VolumeIcon /></Icon>} label="Volume"/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value (controlled). |
min / max | number | 0 / 100 | Bounds. |
onChange | (next: number) => void | — | Fires on adjust. |
icon / label | ReactNode | — | Leading glyph / caption. |
Toggle
A binary switch, D-pad-focusable. Controlled via checked + onChange. The knob slides on a logical margin, so it mirrors correctly under RTL.
Stepper
Adjust a value in discrete steps (glasses have no fine slider). One focus stop: focus it, then swipe left/right (ArrowLeft/Right) to change by step; bounds clamp the ends. Controlled via value + onChange.