Components
Deck
A horizontal paged flow (wizard / onboarding). Controlled via index; shows one page with step dots beneath. Pages advance on pinch / D-pad — never scroll.
Installation
npx @glasskit-ui/cli add deckInstall 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> );}// components/glasskit/deck.tsx"use client";import { Children, useEffect, useRef, useState, type ReactNode } from "react";import { seedFocus, useNeuralBand } from "@glasskit-ui/react";import { cn } from "../lib/utils";import { Progress } from "./progress";/** * <Deck> — a horizontal paged flow (wizard / onboarding). Shows one page at a * time with step dots beneath (reusing Progress). Self-connects to * useNeuralBand: a wristband `swipe` advances to the next page (clamped, no * wrap). Pass `index` to control paging yourself — a controlled Deck never * self-advances; swipes surface through `onIndexChange` instead. * One nav model per screen: pages advance on pinch / D-pad, never scroll. * * Platform note (2026-06): the Display delivers web apps only arrow/Enter * keys + history-back — it does not (yet) dispatch custom gesture events, so * the `neuralband` CustomEvent is a forward-compat seam you (or a future OS) * dispatch. For paging that works on-device today, drive `index` from a * focusable control (Button, Stepper) or your own ArrowLeft/ArrowRight * handling. */export function Deck({ index, defaultIndex = 0, onIndexChange, children, className,}: { /** Controlled page. Omit for uncontrolled (Neural Band swipes advance). */ index?: number; /** Starting page when uncontrolled. */ defaultIndex?: number; /** Fires with the next page on every swipe (both modes). */ onIndexChange?: (index: number) => void; /** One node per page. Falsy children (`{cond && <Page/>}`) are dropped, which * changes the page count — render every page and gate inside it instead. */ children: ReactNode; className?: string;}) { const pages = Children.toArray(children); const count = pages.length; const controlled = index != null; const [internal, setInternal] = useState(defaultIndex); const current = Math.max( 0, Math.min(controlled ? index : internal, count - 1), ); // useNeuralBand is a one-shot (the gesture clears on the next microtask), // so an effect keyed on the gesture alone fires exactly once per physical // swipe — consecutive identical swipes both advance. Everything else is // read through a latest-values ref: with it in the deps, the re-render the // advance itself causes would re-run the effect while the gesture string is // still set and double-advance. const gesture = useNeuralBand(); const latest = useRef({ current, count, controlled, onIndexChange }); latest.current = { current, count, controlled, onIndexChange }; useEffect(() => { if (gesture !== "swipe") return; const { current, count, controlled, onIndexChange } = latest.current; const next = Math.min(current + 1, count - 1); if (next === current) return; if (!controlled) setInternal(next); onIndexChange?.(next); }, [gesture]); // If the focused element lived inside the outgoing page it unmounts on // advance, stranding focus on <body> — reseed the D-pad ring. Only when // orphaned: a focus that survived (e.g. an external Next button) keeps it. const mounted = useRef(false); useEffect(() => { if (!mounted.current) { mounted.current = true; return; } const active = document.activeElement; if (!active || active === document.body) seedFocus(); }, [current]); return ( <div className={cn("gk-deck", className)}> <div className="gk-deck__stage" role="group" aria-label={`Page ${current + 1} of ${count}`} > {pages[current]} </div> {count > 1 ? ( <Progress variant="step" value={current + 1} max={count} /> ) : null} </div> );}Usage
// self-wired: Neural Band swipes advance<Deck> <OnboardConnect /> <OnboardCalibrate /> <OnboardReady /></Deck>// or controlled<Deck index={step} onIndexChange={setStep}>…</Deck>Props
Prop
Type