GlassKit UI

Patterns

Which component when — navigation (Tabs vs Deck vs Navigator), notifications (Toast vs Toaster vs NotificationCard), quantities (Progress vs Meter vs Timer), empty/error/loading states, and the API conventions every component follows.

GlassKit has one screen, one input (the Neural Band → D-pad), and several components that look adjacent. This page is the decision guide.

Pick one navigation model per screen — competing models fight over the same four arrows.

UseWhen
TabsA few peer views the wearer flips between, any order, no hierarchy. The tab bar stays visible — state is glanceable.
DeckA linear, ordered flow: onboarding, a wizard, a workout sequence. Forward-only paging with step dots; no random access.
NavigatorHierarchy — a list that opens a detail, a settings tree. Pushes are real history entries, so the system back gesture (middle pinch, OS v125.1+) pops a screen. The only choice when "back" must mean "up one level".
LauncherThe app's front door: a grid of destinations, each opening one of the above.

Rules of thumb:

  • If the wearer would ever ask "how do I get back?", you want Navigator — Tabs and Deck don't touch history, so back exits the app.
  • Deck inside a Navigator screen is fine (a flow launched from a list). Navigator inside a Deck page is not — a back-pop mid-wizard strands the flow.
  • Don't reach for Deck just because content overflows: that's a List (scrolling is focus-driven) on one screen.

Notifications: Toast vs Toaster vs NotificationCard

UseWhen
ToastOne transient confirmation you render yourself ("Saved", "Sent"). You own mounting and timeout.
ToasterApp-wide toast queue — call toast() from anywhere; it stacks, times out, and renders in a portal. Use this once you have more than one source of toasts.
NotificationCardAn actionable item that waits for the wearer — an incoming message with reply chips, a calendar alert. It participates in focus; a toast never does.

If the wearer must be able to act on it, it's a NotificationCard. If it narrates something that already happened, it's a toast.

Quantities: Progress vs Meter vs Timer

  • Progress — task completion: a download, an upload, step n of m (the step variant). Always moving toward done.
  • Meter — a level that just is: battery, volume, signal. No notion of completion; reads as a gauge.
  • Timer — time remaining: big tabular countdown with an optional drain bar. Self-ticking; onComplete fires at zero.

Loading, empty, and error

AsyncView is the spine — give it status and the three slots:

<AsyncView
  status={status}
  placeholder={
    <EmptyState title="No workouts" hint="Start one on your phone." />
  }
  error={<ErrorState title="No signal" onRetry={refetch} />}
>
  {data}
</AsyncView>
  • EmptyState — nothing failed, there's just no content yet. Quiet treatment, optional invite action.
  • ErrorState — something failed and the wearer can retry. No red: the lens has one accent, so the words carry the failure.
  • The default loading slot is the pulsing Spinner; override it for skeleton-style screens.

Free-form text

There is no keyboard, microphone, or text-input API on the platform. Two working answers, in order of reach:

  1. PickerComposeFlow: activating the field opens a back-gesture-aware screen of choices. Right whenever the space of answers is enumerable (replies, presets, templates).
  2. Phone relay — for genuinely free-form text, the wearer types on their phone: npm create glasskit my-app -- --template relay scaffolds the whole reference — a 6-char pairing code on the lens, a relay page the phone opens, a 90-line dependency-free Node server, and a useRelayText hook that polls the value in.

Both are the seam system dictation would replace (see the platform wishlist) — swap the capture flow, keep the field.

Screen readers

The conventions, so your additions match:

  • Cue is role="status" — the screen's narration line announces politely. Keep one Cue per screen; two live regions talking over each other is noise.
  • Transients announce themselves: Toast, AsyncView, and NotificationCard are role="status"; Timer is role="timer". Don't wrap them in another live region.
  • Rapidly changing values stay silent: Readout, StatGrid, Meter, and Compass would announce on every tick — they expose labels (aria-label/visible text) but are deliberately not live. Narrate milestones through the Cue instead ("Halfway there").
  • World-anchored SVGs (Pin, Callout, Reticle, Compass) are role="img" with meaningful labels — they describe a point in the world, not a control.

Performance

  • The perf budget is the platform's: under 500 KB gzipped JS and 10 requests (CI enforces the bundle budget on every PR).
  • Long lists are cheap by default — rows use content-visibility: auto, so offscreen rows skip layout and paint while keeping honest scroll metrics and focus-engine rects. Past a few hundred rows, paginate per screen anyway: a wearer never scans 300 rows on a lens.
  • Component source is capped at 12 KB (build:registry fails over it) — a vendored component should read in one sitting.
  • Sensor hooks guard setState (a 60 Hz orientation stream doesn't re-render on every fire) — keep that property when composing them.

API conventions (every component follows these)

  • Auto-wiring — sensor-driven components self-connect when their data prop is omitted: <Compass /> follows live head orientation, <Compass heading={290} /> is controlled. The prop always wins. Same shape for DirectionArrow (target), Clock and Timer (self-ticking), Deck (Neural Band swipe).
  • Focus — interactive elements carry the focusable class; useDpad() (called once at the root) does spatial navigation and Enter-to-click. A focused Slider owns ArrowLeft/Right for value adjust — vertical arrows still navigate away. data-autofocus picks where a screen's ring starts; <FocusScope> contains it to a modal subtree (Confirm and PermissionPrompt do this for you); Navigator restores the ring to the row that opened a screen when you come back (focus memory).
  • One task per view — most components are sized to be a screen's main event, not dashboard tiles. If a screen needs three of them, it's probably three screens (see Navigator).
  • World-anchored never mirrors — DirectionArrow, Compass, Pin, Callout, Reticle, and Viewfinder keep physical positioning under RTL; everything else uses logical CSS and flips for free.
  • One word per meaningemphasis is visual weight ("default" | "accent" on Badge, Cue, Toast), status is semantic state ("on" | "live" | "off" on StatusDot), and tone is reserved for the gradient palette (Avatar, GlowIcon plates, Launcher tiles). A prop name tells you what kind of choice you're making.
  • Platform honesty — components that look like device capture (Viewfinder, Dictation, TextField's mic affordance) are presentation: web apps on the Display get no camera, microphone, or gaze API. Their JSDoc says exactly what you own.

On this page