The UI layer is the on-demand half of a two-layer miniapp. It is a normal React app that mounts when the user opens your tile and is destroyed when they leave. It has zero direct hardware access. To reach the glasses it sends messages to its own background layer, which holds the session and translates those messages into session.* calls. The bridge is a single injected global named mentra. The entry point is small: create a React root, wrap it in <MentraProvider>, and call mentra.ready().
src/ui/main.tsx
UI imports come from two sub-paths. @mentra/miniapp/ui re-exports everything (the mentra global types, the React hooks, <MentraProvider>, <MiniappHeader>), and @mentra/miniapp/react exports the hooks and components directly. There is no @mentra/sdk here and no hosted server: the UI never authenticates, never opens a port, and never owns a session.
The UI layer must not construct a MiniappSession. That class is the background (JSContext) API. Inside a WebView its CONNECT never gets acked by the host’s UI router, so session.connect() times out with CONNECT_ACK timeout. UI code reaches the glasses only through mentra, and reads host state through the hooks below.

The mentra global

The host injects mentra into every UI WebView before your content loads. It carries the typed bus to background plus the two lifecycle signals.
The two sides are asymmetric on purpose. mentra.send from the UI buffers until ready(), while session.ui.send from the background drops silently when no WebView is open. See Talking between layers for the background side (session.ui.handle, on, send). mentra.request rejects with an Error whose name is MentraRpcError (the handler threw), MentraRpcTimeoutError (the timeout elapsed), or AbortError (the signal aborted). Branch on err.name, not instanceof: these errors come from the WebView’s bare runtime scope, so instanceof checks are unreliable across the bridge.

Typed channels

Declare every channel once in src/shared/channels.ts. Both halves import this file, so channel names and payload shapes are checked at compile time on both sides of the bridge. MentraTyped<Channels> types the global; importing the file for its declare global side effect is what wires the types in.
src/shared/channels.ts
For request/response channels, mark the entry as an RPC shape so mentra.request (and useRpc) infer the request and response types. send/on then reject that channel at compile time, and request rejects the broadcast channels.

React hooks

Each hook is a thin read over host-injected state or the mentra bridge. The host injects safe-area and color-scheme values once before content loads, so those hooks read at mount and do not update at runtime (a theme or orientation change forces a reload). useRpc recreates its AbortController per call: calling it again aborts the previous in-flight request (good for per-keystroke autocomplete), and unmount aborts everything pending. A caller-provided options.signal is merged with the internal one.
useCapabilities returns null by design. It does not construct a MiniappSession (which would time out in a WebView). When a UI page needs the glasses profile, read session.capabilities in a background session.ui.onOpen handler, session.ui.send it on a channel, and pick it up in the UI with mentra.on.

Components

<MentraProvider>

The optional root provider. Today it keeps <html class="dark"> in sync with the host’s color scheme so shared theme CSS switches automatically. The toggle runs synchronously during render, so children paint with the right class on first mount and there is no theme flash. Pass syncColorScheme={false} if your app owns the dark class itself (for example when integrating a theming library).

<MiniappHeader>

A drop-in header that aligns with the host’s capsule menu, leaving the top-right clear. It has three slots: left, title, and right. A string title renders as a semantic <h1>; pass a node for custom markup. Set onBack to render a back chevron in the left slot (ignored if left is set). The component layers useCapsuleHeaderStyle underneath, so its UseCapsuleHeaderStyleOptions (leftPadding, rightGap, fallbackHeight, fallbackMarginTop) are accepted as props.
Use useCapsuleHeaderStyle directly when you want full control over header markup but want the alignment math done for you. For the capsule-menu rect, the safe-area insets, and where you may not place clickable content, see Safe Areas & Layout.

Next steps

Two-layer architecture

The bus model and how background and UI talk.

Safe areas & layout

Keep content clear of the capsule menu.

The session

The background-side session your UI sends to.

Interop & Actions

Let other miniapps call yours.