GlassKit

glasses-ui API reference

The typed 600×600 + D-pad + sensor primitives Meta doesn't ship. Public API of @glasskit/glasses-ui.

@glasskit/glasses-ui is the typed component library every glasses app imports. It's workspace-only, not published to npm; the source lives at packages/glasses-ui/src/.

Its job is to make the platform's awkward bits (additive display, no pointer, D-pad-only focus, W3C sensors) feel like normal React. You get one container component, one focus hook, four sensor hooks, two pure utilities, and a stylesheet.

import {
  GlassViewport,
  useDpad,
  useDeviceOrientation,
  useDeviceMotion,
  useGeolocation,
  useNeuralBand,
} from "@glasskit/glasses-ui";
import "@glasskit/glasses-ui/styles.css";

Stylesheet is required

It defines the additive-display palette, the .screen / .focusable / .launcher-* semantic classes, and the focus ring. Import once at your app entry.


<GlassViewport>

The 600×600 root container. Wrap your whole glasses app in it.

<GlassViewport>
  <App />
</GlassViewport>

Props

Prop

Type

Design rules baked in

The Meta Ray-Ban Display is additive: pure black emits no light, so black is transparent (the wearer sees the real world) and everything you draw is light layered on top. The viewport enforces:

  • Pure black background (#000): don't fill it
  • Few elements, high contrast, generous spacing
  • Light is the ink: green and white render best

This is the design system, not a config option. Bright marks on pure black. Anything else (gradients, gray-on-gray cards) looks muddy through the optics.


useDpad()

Wires the D-pad (Neural Band arrows + Enter) to focus navigation. Call it once, near the app root.

function App() {
  useDpad();
  return (
    <GlassViewport>
      <button className="focusable">One</button>
      <button className="focusable">Two</button>
    </GlassViewport>
  );
}

How it works

  • Anything with className="focusable" participates
  • Arrow keys (or the Neural Band's swipes, which arrive as arrow keys at the platform level) move focus spatially, picking the nearest focusable in the pressed direction using a scored algorithm that balances "distance along the travel direction" against "drift perpendicular to it"
  • Enter or Space on the focused element fires its onClick
  • The first focusable on mount auto-receives focus
  • Disabled elements (disabled or aria-disabled="true") are skipped

Adding focusable

Any element. Buttons most commonly, but cards, links, list items work too:

<button className="focusable" onClick={onPick}>…</button>
<div className="focusable" role="button" tabIndex={0} onClick={onPick}>…</div>

The class only registers the element with useDpad. Styling for the focus ring comes from styles.css (it draws a 2px primary-color outline on :focus-visible for any .focusable).

scoreRect + Dir type: exported for testing

The pure scoring function is exported separately so you can unit-test custom focus layouts:

import { scoreRect, type Dir } from "@glasskit/glasses-ui";

scoreRect(currentRect, candidateRect, "right");
// → null  (candidate isn't to the right)
// → number (lower = better fit)

The library's own packages/glasses-ui/src/dpad.test.ts is the reference for how to test this. You probably never call scoreRect yourself outside tests.


useDeviceOrientation()

Compass heading + tilt, in degrees. Wraps W3C deviceorientation events.

const { alpha, beta, gamma } = useDeviceOrientation();

Returns

Prop

Type

Returns null for each axis before the first event fires (sensor needs a moment to start producing values).

The hook is setState-guarded so it only re-renders when a value actually changes. That's critical at the 600×600 surface's tight perf budget, since orientation events fire at ~60 Hz.

Local testing

Drive with Chrome DevTools → ⋮ → More tools → Sensors. Set "Orientation" to a custom value and your alpha updates.


useDeviceMotion()

Acceleration including gravity, in m/s² per axis. Wraps W3C devicemotion.

const { x, y, z } = useDeviceMotion();

Returns

Prop

Type

The demos use it for effort detection:

const mag = motion.x != null && motion.y != null && motion.z != null
  ? Math.abs(Math.hypot(motion.x, motion.y, motion.z) - 9.8)
  : null;
const effort = mag == null ? "unknown" : mag < 1 ? "low" : mag < 4 ? "moderate" : "high";

(Subtract 9.8 to remove gravity; the remainder is "how much the user is moving.")

Same setState-guard pattern as orientation: re-renders only when a value changes.

No built-in DevTools simulation

Either test on-device (recommended for any motion-driven demo) or write a small window.setInterval shim that dispatches synthetic devicemotion events during dev.


useGeolocation()

Live GPS. Wraps navigator.geolocation.watchPosition.

const { position, error } = useGeolocation();

Returns

Prop

Type

Hide UI that needs coordinates behind if (!position) return .... accuracy is the radius in meters, useful for showing "±15 m" status indicators.

The hook configures enableHighAccuracy: true + a 5-second maximumAge (cached fixes from the last 5 seconds are acceptable; older requires a fresh fix). It cleans up the watch on unmount and guards against the OS firing callbacks after the component leaves.

Local testing

Chrome DevTools → Sensors → Geolocation. Pick a city preset or paste lat/lon.


useNeuralBand()

The wristband's richer gestures: pinch, double-pinch, swipe.

const gesture = useNeuralBand();

useEffect(() => {
  if (gesture === "pinch") advance();
}, [gesture]);

Returns the gesture string for one render, then clears to null on the next microtask. That way useEffect(..., [gesture]) fires for every event, even two consecutive identical pinches.

Why this is a separate hook from useDpad

The Neural Band's swipes + click arrive as arrow keys + Enter at the platform level, which is what useDpad consumes. That covers ~80% of input. This hook is for the rest: pinch and swipe events that the platform exposes as a neuralband CustomEvent on window instead of as keys.

Simulating in dev

The platform fires the CustomEvent on real glasses. In the browser you fire it yourself:

window.dispatchEvent(
  new CustomEvent("neuralband", { detail: { gesture: "pinch" } }),
);

Useful for testing recipe-advance / presentation-advance flows without the hardware.

What gestures are supported

Whatever Meta's platform fires. The hook is a pass-through, not an opinionated whitelist. "pinch" is the one shipped in every demo today. "double_pinch", "swipe_left", "swipe_right", etc. may be added by Meta over time without library changes; you only need to extend your switch.


orientationEqual / motionEqual

Pure equality helpers exported for the same reason scoreRect is: unit-testability of the sensor hooks. They check whether two orientation (or motion) snapshots are field-by-field identical, so the hooks can skip setState when nothing changed.

import { orientationEqual, motionEqual } from "@glasskit/glasses-ui";

You won't call these directly unless you're writing custom sensor plumbing.


The stylesheet (styles.css)

Imported once at your app entry:

import "@glasskit/glasses-ui/styles.css";

Provides:

  • The additive-display @theme tokens: bg / accent / ink / dim colors tuned for the optics
  • .glass-viewport + .glass-viewport--frame: the 600×600 container styling
  • .screen: flex column, padding, the standard demo layout
  • .focusable: base styles + the focus ring on :focus-visible
  • .launcher, .launcher-grid, .launcher-card, .launcher-card__label, .launcher-card__tagline: the demo launcher grid
  • .app-head, .row, .readout, .label, .dim, .cue-script, .cue-line, .cue-bar: the semantic typography + layout classes the demos use

You can extend, override, or replace these in your fork's own CSS. They're plain Tailwind v4 @layer base rules with the additive palette. See Theming.


When to extend the library

The library stays small on purpose. If your demo needs a new primitive (e.g. a useCompass() that's specifically heading-only, or a <RingProgress> component), add it INSIDE your fork's app/src/lib/ first. Only promote to @glasskit/glasses-ui if it's truly platform-shaped (sensors, focus, viewport conventions) and would benefit every GlassKit developer.

If you do promote something, run pnpm --filter @glasskit/glasses-ui test to make sure the existing test suite still passes. The dpad spatial-scoring algorithm is the trickiest piece and has good coverage.

On this page