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 underline. Controlled via value + onChange.
Installation
npx @glasskit-ui/cli add tabsInstall 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/tabs.tsximport type { ReactNode } from "react";import { cn } from "../lib/utils";export type TabItem = { id: string; label: ReactNode };/** * <Tabs> — a top-level tab strip (the home's quick-controls | home | apps * pager). Anchor it at the top of the view — pass it as <Screen>'s `status` * slot — so context stays glanceable above the content it switches. * Selected = accent underline; focused = the system focus ring (two distinct * affordances). Controlled via `value` + `onChange`. RTL-safe (logical * layout). * * Deliberate ARIA deviation: no `aria-controls`/`tabpanel` wiring — on the * lens a tab switch swaps the whole 600×600 screen, so there is no co-rendered * panel to point at. */export function Tabs({ items, value, onChange, className,}: { items: TabItem[]; value: string; onChange?: (id: string) => void; className?: string;}) { return ( <div className={cn("gk-tabs", className)} role="tablist"> {items.map((t) => { const on = t.id === value; return ( <button key={t.id} type="button" role="tab" aria-selected={on} onClick={onChange ? () => onChange(t.id) : undefined} className={cn("focusable gk-tab t-body", on && "gk-tab--on")} > {t.label} </button> ); })} </div> );}Usage
<Tabs value={tab} onChange={setTab} items={[ { id: "controls", label: "Controls" }, { id: "home", label: "Home" }, { id: "apps", label: "Apps" }, ]}/>Props
Prop
Type
Navigator
A screen stack with system-back integration. Every push adds a real history entry, so the Display's back gesture pops it via popstate. The stack rides in history.state — a mid-flow reload restores the screen; opt-in paths mirror pushes into the URL. Pop restores focus to the row that pushed.
Callout
A world-object annotation: an anchor + a vertical leader up to an emitted label (no box — just a leader line + emitted type). Project x from relative bearing like Pin (lib/geo). World-anchored, never mirrored.