Glassly Miniapp SDK betaThe SDK is in beta, so its APIs may change before general availability.Developing on Mentra Live? We recommend using the Glassly Bluetooth SDK.The developer tools are available in the Glassly App under Settings → Miniapp Developer Settings.Share feedback with an in-app bug report, on Discord, or by email at [email protected].
session.display.render() draws to the glasses HUD. Each call describes the whole frame: everything that should be on screen right now. The phone diffs it against the previous frame, so elements with a stable id update in place (no flicker), anything you leave out of the new frame comes off the screen, and render([]) clears it. Your job is to describe the frame; the phone works out what changed.
Display needs no permission, but the glasses do need a screen. Declare a DISPLAY hardware requirement in your manifest so the miniapp only installs on glasses that can show anything.

Elements

Every element also takes rotation/pivot (shapes and SVG) and depth (any element), described below. Shapes beyond rect render where capabilities.display.shapes lists the type (the Even Realities G2 with the Glassly firmware draws all of them on the glasses); elsewhere they are dropped and reported. color is a gray level 0..15. Points are raw canvas pixels and are not anchored.

Text fonts

style.font picks how text is drawn: style.fontSize (default 20) sizes font: "mono" text.

Rotation

Geometric shapes and svg elements take a rotation in clockwise degrees, unwrapped, so 0 → 360 is a full turn. pivot fixes the centre of rotation in canvas pixels; it defaults to the target box’s centre. Text and image elements ignore both.
Because rotation tweens like any other geometry change, giving a rotating element a stable id and a transition spins it on the glasses.

Depth

Every element takes an optional depth: a stereo disparity in canvas pixels from -16 to 16. Positive values float the element in front of the rest of the frame, negative values sink it behind. Devices whose capabilities.display.depth is true draw the element depth px further right on the left lens and depth px further left on the right lens; every other device draws it flat, at the same place. Depth never drops an element. Depth is a cue, not an order: elements still paint in array order. A card that should sit on top of running content therefore goes last in the array and at a positive depth. Because it is only a per-lens offset, a card slides in over the content with the ordinary transition mechanics:
A depth change with a transition glides toward or away from the viewer: each lens moves the element the opposite way, so it comes closer without changing size. Text uses style.font: "builtin" in the example so a moving box never re-wraps it on the phone.

Transitions

Give an element a stable id and a transition, and geometry changes between renders animate on devices with capabilities.display.animation:
Moves, resizes, point and angle changes, color and stroke widths tween; new text or image pixels switch immediately. Devices without animation jump. Text with a stable id can therefore tick, scroll or fade at tens of updates per second: re-render with the new string and, optionally, a new box.

Screen sizes

box is {x, y, w, h} in raw device pixels. Coordinates map 1:1 to the device canvas, with the origin at the top-left: We recommend designing your layout for 500 × 220. A frame that fits 500 × 220 renders as-is on every positioning device; on the G2’s larger canvas it sits in the top-left with some margin. Boxes that run past the edge are clamped, and elements past the device’s budget are dropped tail-first. Both show up in the render result, so you find out.

Anchoring to a corner

For elements that hug a screen corner (a minimap bottom-right, a status glyph bottom-left), give the box an anchor. x/y then become insets from that corner, growing inward, and the phone resolves the box against the connected device’s real canvas. The element lands flush on every resolution with nothing to clamp:
anchor is one of "top-left" (the default), "top-right", "bottom-left", "bottom-right". Corners only, there is no center or edge anchoring, and no scaling. When you want an exact fit instead (a full-screen caption), compute boxes from the connected device’s real canvas. session.capabilities.display carries it, populated on the "ready" event:

Older glasses

Even Realities G1 and Vuzix Z100 show text but can’t place elements at coordinates, and their displays can’t show bitmaps. Render the same scene anyway: the phone converts it for them. Text elements collapse into a single full-view text layout in reading order (top to bottom, then left to right), and image and rect elements are dropped. The result tells you what happened (degraded: true, plus the dropped ids), and session.capabilities.display.canPosition tells you up front which kind of device you’re on. In practice a text-first miniapp works everywhere with zero branching: a caption app’s single full-canvas text element reads the same on every device. If your layout leans on positioning or images, like a HUD with a minimap, check canPosition and render a text-only variant for these devices.

Updating and clearing

Give any element that changes over time a stable id. Re-rendering with the same id updates that element in place on the glasses, and unchanged elements never re-cross Bluetooth, so pushing the whole frame on every change is cheap. To take something off screen, leave it out of the next frame. To clear everything:

Options and results

render(elements, options?) takes {view, durationMs}. view picks "main" (default) or "dashboard", and durationMs auto-clears the frame after that many milliseconds. Awaiting is opt-in. A plain fire-and-forget call is fine, and the returned promise never rejects:

Just showing text

The simplest useful frame is one text element covering the whole canvas: a status line, a caption, a “Saved” confirmation.
Per-element style.breakMode ("character" | "character-no-hyphen" | "word" | "strict-word") controls how the text wraps inside its box.

Helpers

Two optional helpers ship from @glassly/miniapp:
monoTextRows({id, text, box, fontSize}) splits a fixed-pitch block into one text element per line, each with a stable ${id}-row-N id, using the G2 mono renderer’s metrics. On G2 custom firmware a line containing unsupported Unicode then rasterizes only its own row instead of the whole grid. It’s opt-in: one row costs one text element, so check the device’s text budget first, and include every returned row (blanks included) in each replacement scene. new LatestRenderScheduler(() => intervalMs) paces a producer that can generate frames faster than the glasses can show them. request(fn) keeps at most one render in flight and one replaceable pending callback, so no scene is built until it can be sent; clearPending() drops the queued one and dispose() stops the scheduler. It bounds SDK work, not Bluetooth traffic, and never cancels work already handed to the phone.

Text measurement and pixel art

@glassly/miniapp/display now exposes the same pure TypeScript text primitives the host uses. They work in a miniapp background without DOM or native imports:
Choose G1_PROFILE, G1_PROFILE_LEGACY, G2_PROFILE, Z100_PROFILE, or NEX_PROFILE (Mentra Display) for your target’s text metrics. Read live capabilities for the canvas size and element budgets, and pass the text box’s width and line limit explicitly. Profiles do not measure arbitrary browser fonts or font: "mono" at a custom size. measureText(text) returns pixel width. wrap() returns lines, truncated, maxLineWidthPx, totalBytes, and per-line lineMetrics, plus the original text and chosen break mode. wrapToLines() returns just the lines. Wrapping also accepts maxBytes; its default break mode is "character-no-hyphen". Pixel-art rows must have equal widths: . is black, # is full intensity, and 0–f are gray levels. pixelArtPng(rows) returns base64 PNG data; pixelArtSize(rows) returns {w, h}. Keep the rows array immutable and reuse it so encoding can be cached. Image support and element budgets still apply. The subpath also exports shared scene types such as SceneElementInput, SceneFrame, and SceneTransition.

Dashboard view

The dashboard view is one overlay slot above retained main content. The dashboard is a role that one installed miniapp holds at a time, the active dashboard. A miniapp must declare manifest type: "dashboard" to be eligible, and the wearer picks the active one in the Glassly App under Settings > Dashboard > Dashboard app (the picker appears once two or more dashboards are installed). The default is the bundled com.glassly.dashboard, and uninstalling the active dashboard returns the role to it. Only the active dashboard can take the slot, and only while session.capabilities?.display?.views includes "dashboard". Currently this requires connected glasses running Glassly CFW and the wearer’s dashboard setting enabled. Check capabilities again when they change. A dashboard that does not hold the role is not started by alwaysOn. showView("dashboard") takes the slot; showView("main") releases it and restores retained main content. Both are idempotent and resolve with {status: "shown" | "unsupported", reason?} rather than rejecting. A dashboard miniapp that does not hold the role gets reason: "not the active dashboard"; a miniapp of any other type gets reason: 'The dashboard view belongs to miniapps of type "dashboard"'. Rendering with {view: "dashboard"} alone does not acquire the slot.
When the dashboard role moves to another miniapp, the previous holder receives onViewChange({view: "main", reason: "replaced"}). Other reasons are "shown", "hidden", and "ineligible". "app_stopped" is included in the reason type but is not delivered to a stopped miniapp. onViewChange returns an unsubscribe function and fires only for the active dashboard. Release temporary gesture claims when a page loses the view.

Native dashboard

session.dashboard.showNativeDashboard() is a different thing: it brings the glasses’ own OS-level idle screen to the foreground. On G2 this tears down the current page, and the display comes back when the wearer dismisses the dashboard or you render again. It resolves with {status: "shown" | "unsupported", reason?} and never rejects; "unsupported" means the host refused, for instance because the native nav UI owns the screen. session.dashboard.setContent() remains a deferred widget API and has no visible effect. Use session.display for overlay scenes.

Native navigation UI

Glasses that report capabilities.hasNativeNavigation (the G2) have a firmware-drawn turn-by-turn screen. Three display methods feed it while a trip is active. Each resolves with {status: "sent" | "unsupported" | "blocked", reason?} and never rejects; on "unsupported" there is no native nav UI or no active trip, so fall back to render().
Every NativeNavInfo field but directionIcon is a pre-formatted display string the device renders verbatim, so format units and times yourself (session.preferences carries the user’s unit system and clock). sendNativeNavMiniMap(data, {rotationDeg?, border?}) pushes the live mini map as a base64 BMP or PNG; the phone re-encodes and scales it to the device’s slot (176 × 176 on G2). Draw it north-up with the cursor centred and let the device rotate it by rotationDeg. sendNativeNavOverviewMap(data) pushes the full-route overview map (576 × 188 on G2).