Copy for LLM
Deck
A horizontal paged flow (wizard, onboarding). Shows one page with step dots beneath. Uncontrolled with Neural Band swipe to advance, or controlled via index and onIndexChange. Pages advance on pinch or 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.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> );}// 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("flex w-full flex-col items-center gap-5", className)}> <div className="flex flex-col items-center gap-3 text-center" 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 | Default | Description |
|---|---|---|---|
index | number | — | Controlled page (clamped). Omit for uncontrolled, where Neural Band swipes advance. |
defaultIndex | number | 0 | Starting page when uncontrolled. |
onIndexChange | (index: number) => void | — | Fires with the next page on every swipe (both modes). |
children | ReactNode | — | One node per page. Falsy children are dropped, which changes the page count, so render every page and gate inside it instead. |
className | string | — | Classes merged onto the outer wrapper. |
How to use it
Deck is forward-leaning paging for a linear, ordered flow: onboarding, a
setup wizard, a workout sequence. It shows one page at a time with step dots and
no random access. Use it when the steps have an order the wearer moves through,
not when content simply overflows (that is a List).
Uncontrolled: swipe to advance
By default Deck manages its own page. The Neural Band swipe gesture advances
to the next page (clamped at the ends), so the wearer flips through with the
wristband. Pass defaultIndex to start somewhere other than the first page, and
onIndexChange to observe each move.
import { Deck } from "@/components/ui/deck";
<Deck onIndexChange={(i) => track("onboarding_step", i)}>
<Welcome />
<Permissions />
<Done />
</Deck>;Controlled: you own the index
Pass index to drive the page yourself, for example from your own "Next"
button. In controlled mode Deck never self-advances; swipes surface through
onIndexChange and you decide what to do.
const [step, setStep] = useState(0);
<Deck index={step} onIndexChange={setStep}>
<Welcome />
<Permissions />
<Done />
</Deck>;Deck and the back gesture
Deck does not push history, so the back gesture leaves the app rather than stepping back a page. If a flow needs back to mean "previous step," either drive the index yourself and register a useBackHandler in the hosting Navigator, or rethink whether the flow belongs inside a Navigator screen. The common shape is a Deck inside a Navigator screen: back pops the whole flow at once, which is usually what the wearer wants.
Tabs
A top-level tab strip (the home's quick-controls, home, apps pager). Each tab is D-pad-focusable; the active one gets an accent indicator. Controlled via value and onChange. Tabs do not touch history, so back exits the app.
Launcher
The app grid: the entry screen for a multi-app surface. Two columns of D-pad-focusable cards on gradient icon plates. Keep it to about six apps so the whole grid is one glance.