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.
Navigation: Tabs vs Deck vs Navigator vs Launcher
Pick one navigation model per screen — competing models fight over the same four arrows.
| Use | When |
|---|---|
| Tabs | A few peer views the wearer flips between, any order, no hierarchy. The tab bar stays visible — state is glanceable. |
| Deck | A linear, ordered flow: onboarding, a wizard, a workout sequence. Forward-only paging with step dots; no random access. |
| Navigator | Hierarchy — 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". |
| Launcher | The 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
| Use | When |
|---|---|
| Toast | One transient confirmation you render yourself ("Saved", "Sent"). You own mounting and timeout. |
| Toaster | App-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. |
| NotificationCard | An 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
stepvariant). 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;
onCompletefires 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
loadingslot is the pulsingSpinner; 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:
- Picker —
ComposeFlow: activating the field opens a back-gesture-aware screen of choices. Right whenever the space of answers is enumerable (replies, presets, templates). - Phone relay — for genuinely free-form text, the wearer types on their
phone:
npm create glasskit my-app -- --template relayscaffolds 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 auseRelayTexthook 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:
Cueisrole="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 isrole="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:registryfails 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
focusableclass;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-autofocuspicks 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 meaning —
emphasisis visual weight ("default" | "accent"on Badge, Cue, Toast),statusis semantic state ("on" | "live" | "off"on StatusDot), andtoneis 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.
Getting started
GlassKit UI — the React component library for Meta Ray-Ban Display apps. Scaffold a glasses app in one command, or add the SDK and vendor 48 premium HUD components with the CLI.
Example apps
Complete multi-screen apps composed from the real components — drive them with your keyboard here, or scan the QR and run them on your glasses. Workout (Navigator + Timer + destructive Confirm) and Messages (threads + ComposeFlow replies).