GlassKit UI
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.

Discard recording?

12:48 of footage will be permanently deleted. This can't be undone.

Installation

npx @glasskit-ui/cli add confirm

Install 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

PropTypeDefaultDescription
titleReactNodeThe question.
messageReactNodeSupporting detail.
confirmLabel / cancelLabelReactNode"Confirm" / "Cancel"Button labels.
onConfirm / onCancel() => voidAction handlers.
destructivebooleanfalseIrreversible action. Seed the D-pad ring on cancel, not confirm.
classNamestringExtra classes.

On this page