Primitives
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.
Installation
npx @glasskit-ui/cli add confirmInstall 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/button.tsximport type { ReactNode } from "react";import { cn } from "../lib/utils";/** * <Button> — a D-pad-focusable action. Renders a real <button> carrying * the `focusable` class, so `useDpad()` includes it in spatial navigation * and activates it on Enter/Space (the hook calls `.click()`, which fires * `onClick`). `primary` wears the accent fill; `positive`/`danger` carry the * semantic accept/destroy fills (calls, irreversible confirms); `ghost` is * chrome-less (just text + focus ring + press) for toolbars and icon rows. */export function Button({ children, variant = "secondary", icon, disabled, onClick, type = "button", initialFocus = false, "aria-label": ariaLabel, className,}: { /** The label. Omit for an icon-only button, but then set `aria-label`. */ children?: ReactNode; variant?: "primary" | "secondary" | "ghost" | "positive" | "danger"; /** Optional leading glyph — typically a <Icon>. */ icon?: ReactNode; disabled?: boolean; onClick?: () => void; type?: "button" | "submit" | "reset"; /** Seed the D-pad ring here when the screen mounts (`data-autofocus`). */ initialFocus?: boolean; /** Accessible name — required for an icon-only button. */ "aria-label"?: string; className?: string;}) { return ( <button type={type} disabled={disabled} onClick={onClick} aria-label={ariaLabel} data-autofocus={initialFocus || undefined} className={cn( "focusable press-scale t-body inline-flex items-center justify-center gap-2 rounded-2xl", variant === "primary" ? "btn-primary" : variant === "positive" ? "btn-positive" : variant === "danger" ? "btn-danger" : variant === "ghost" ? "bg-transparent border-0 text-foreground" : "surface", !children ? "p-[13px] [&_svg]:size-[22px]" : "px-6 py-4", className, )} > {icon} {children} </button> );}// components/glasskit/confirm.tsx"use client";import type { ReactNode } from "react";import { FocusScope } from "@glasskit-ui/react";import { cn } from "../lib/utils";import { Button } from "./button";/** * <Confirm> — a decision screen: a prompt + a two-button action bar. The * affirmative action sits at the logical end (cancel first, confirm last — * right in LTR, flips for RTL), the library-wide order for action pairs. Drop * it into a <Screen> stage. Focus seeds on the primary action; set * `destructive` for irreversible decisions and the ring seeds on cancel * instead — a blind pinch must never destroy anything. Rendered inside a * FocusScope so the ring can't wander off the decision. */export function Confirm({ title, message, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = false, onConfirm, onCancel, className,}: { title?: ReactNode; message?: ReactNode; confirmLabel?: ReactNode; cancelLabel?: ReactNode; /** Irreversible action — seed the D-pad ring on cancel, not confirm. */ destructive?: boolean; onConfirm?: () => void; onCancel?: () => void; className?: string;}) { return ( <FocusScope> <div className={cn( "flex flex-col items-center gap-4 text-center", className, )} > {title != null ? <p className="t-title">{title}</p> : null} {message != null ? ( <p className="t-body max-w-[28ch] text-muted-foreground">{message}</p> ) : null} <div className="mt-1.5 flex gap-3"> <Button initialFocus={destructive} onClick={onCancel}> {cancelLabel} </Button> <Button variant={destructive ? "danger" : "primary"} initialFocus={!destructive} onClick={onConfirm} > {confirmLabel} </Button> </div> </div> </FocusScope> );}Usage
<Confirm title="End workout?" message="Your route so far will be saved." confirmLabel="End" onConfirm={end} onCancel={dismiss}/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The question. |
message | ReactNode | — | Supporting detail. |
confirmLabel / cancelLabel | ReactNode | "Confirm" / "Cancel" | Button labels. |
onConfirm / onCancel | () => void | — | Action handlers. |
destructive | boolean | false | Irreversible action. Seed the D-pad ring on cancel, not confirm. |
className | string | — | Extra classes. |
QuickReplyChips
Tappable canned replies (the comms job: there is no keyboard on the lens, text is voice). Each chip is D-pad-focusable. Keep the set short and glanceable.
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.