Theme:

UI

Unia uses HTML and CSS by default for UI panels, rendered with WebGPU as full 3D objects (not overlays), so they support occlusion culling and work natively in XR.

Layout is powered by Taffy. Around 95% of common CSS/HTML features are supported.

Quick Start

const container = new UI.Panel(512, 512, 2.0);
this.add(container);

const init = async () => {
  await container.init();
  const doc = container.document;

  doc.addCSS(`
    .panel { background: #1c1c1f; border-radius: 12px; padding: 16px; }
    .title { color: white; font-size: 24px; font-weight: 700; }
    .btn   { background: #303036; border-radius: 8px; padding: 12px 20px;
             color: #f4f4f6; font-size: 16px; }
    .btn:hover { background: #3c3c42; }
  `);

  const panel = doc.createElement("div");
  panel.className = "panel";

  const title = doc.createElement("div");
  title.className = "title";
  title.textContent = "Hello World";
  panel.appendChild(title);

  const btn = doc.createElement("button");
  btn.className = "btn";
  btn.textContent = "Click Me";
  btn.onclick = () => console.log("clicked!");
  panel.appendChild(btn);

  doc.body.appendChild(panel);
};

init();

Panels auto-render when the DOM changes. Multiple edits in the same synchronous block coalesce into a single render.

UI.Panel

new UI.Panel(pixelWidth, pixelHeight, dpr?)
  • pixelWidth / pixelHeight — Texture resolution in CSS pixels
  • dpr — Device pixel ratio (default 1). Use 2 for sharp text, 0.6 for lower-res panels

Extends Object3D — position, rotate, and scale it like any scene object. The quad aspect ratio is set automatically from the pixel dimensions.

Methods

  • await init() — Initialize GPU resources, layout, and fonts. Must be called first.
  • setContent(html, css) — Parse raw HTML/CSS strings (static content path)
  • await update() — Re-render after setContent()
  • await resize(width, height) — Change resolution
  • dispose() — Free GPU resources

Properties

  • document — The document instance (DOM API below)
  • hitbox — Invisible mesh for engine raycasting. Add via world.addRaycastable(ui.hitbox) to enable interaction.
  • pixelWidth / pixelHeight / dpr

Document API

ui.document is a DOM-like API for building panels programmatically.

  • createElement(tag) — Create an element (div, button, span, img, input, etc.)
  • createTextNode(text) — Create a text node
  • addCSS(css) — Append CSS rules (accumulates across calls)
  • querySelector(sel) / querySelectorAll(sel)
  • body — Root element, append your content here

Elements

Elements mirror the browser DOM API.

Properties

el.className = "panel active";
el.id = "main";
el.textContent = "Hello"; // clears children
el.innerHTML = ""; // clears children
el.style.backgroundColor = "red"; // inline style, highest specificity
el.tagName; // 'DIV', 'BUTTON', etc.
el.parentElement;
el.children;
el.firstChild;
el.dataset.value = "42"; // data-value="42"

DOM Manipulation

parent.appendChild(child);
parent.insertBefore(newChild, refChild);
parent.replaceChild(newChild, oldChild);
parent.removeChild(child);
el.remove();
el.cloneNode(deep);
el.contains(other);

Query & Traversal

el.matches(".panel");
el.closest(".wrapper");
el.querySelector(".child");
el.querySelectorAll(".child");
el.getElementsByClassName("cls");
el.getElementsByTagName("div");

Attributes & Classes

el.setAttribute("src", "img.png");
el.getAttribute("src");
el.removeAttribute("src");
el.hasAttribute("src");

el.classList.add("active");
el.classList.remove("active");
el.classList.toggle("active");
el.classList.contains("active");

Events

el.onclick = (e) => { ... }
el.onmousedown = (e) => { ... }
el.onmouseup = (e) => { ... }
el.onmousemove = (e) => { ... }
el.onmouseenter = (e) => { ... }
el.onmouseleave = (e) => { ... }

el.addEventListener('click', handler);
el.removeEventListener('click', handler);

Events bubble up the tree. Call e.stopPropagation() to stop bubbling. Event objects include clientX, clientY, target, and hand.

XR Hand

In XR, e.hand is the controller that triggered the event ('left' or 'right'). On desktop it's null. Use it to pulse the correct controller:

btn.onclick = (e) => {
  if (e.hand) controls[e.hand].pulse(1.0, 30);
  console.log("clicked!");
};

Geometry

el.getBoundingClientRect();
// { x, y, width, height, top, left, right, bottom }

Inline Styles

Set styles directly via the style proxy:

el.style.backgroundColor = "#303036";
el.style.padding = "22px 20px";
el.style.display = "flex";
el.style.gap = "12px";
el.style.width = "200px";
el.style.fontSize = "44px";
el.style.borderRadius = "20px";

Inline styles override CSS rules.

Static HTML Mode

For non-interactive or simple panels, pass raw HTML and CSS directly:

ui.setContent(
  '<div class="card"><h1>Title</h1><p>Body text</p></div>',
  ".card { background: #1c1c1f; padding: 20px; border-radius: 12px; }"
);
await ui.update();

Supported CSS

Selectors

Selector Example Supported
Tag / Class / ID / Universal div, .panel, #main, * Yes
Descendant / Child / Sibling div .child, div > .child, .a + .b, .a ~ .b Yes
Attribute [src], [type=text] Yes
Compound div.panel#main Yes
Comma groups .a, .b Yes
:hover, :root .btn:hover Yes
:nth-child, :not, ::before, ::after No

Layout Properties

Property Values
display block, flex, grid, inline, none
flex-direction row, column
flex-wrap wrap, nowrap
justify-content flex-start, center, flex-end, space-between, space-around, space-evenly
align-items flex-start, center, flex-end, stretch, baseline
flex shorthand, flex-grow, flex-shrink, flex-basis
gap px
width, height, min-*, max-* px, %, auto, none
margin px, auto (centering)
padding px
position static, relative, absolute, fixed
top, right, bottom, left px, auto
overflow visible, hidden, auto, scroll
box-sizing border-box, content-box

Visual Properties

Property Values
background / background-color hex, rgb(), rgba(), named colors, linear-gradient(), radial-gradient()
color hex, rgb(), rgba(), named colors
border width style color shorthand
border-radius px
border-width / border-color per-side supported
border-style solid, none
box-shadow offsetX offsetY blur spread color
font-size px, em, rem
font-weight 400, 700, bold, normal
font-family see Fonts
font-style normal, italic
text-align left, center, right
text-transform uppercase, lowercase, capitalize
white-space normal, nowrap, pre
line-height normal, unitless multiplier, px
letter-spacing px
visibility visible, hidden

Other

  • Gradientslinear-gradient() and radial-gradient() with up to 8 color stops. Linear gradients support angle notation (45deg, 0.5turn) and directional keywords (to top right).
  • CSS variablesvar(--name, fallback) supported; inherits from parent elements.
  • Shorthandmargin, padding, border expand to per-side. border-top and friends also work.
  • Unitspx, em, rem, %.
  • Inheritance — Color, font properties, line-height, letter-spacing, text-align, text-transform, white-space, and visibility inherit.
  • XR Hapticsxr-haptic triggers controller vibration on hover (see XR Haptic Feedback below).

HTML Elements

Standard elements render as expected: div, span, p, h1-h6, button, a, ul, ol, li, hr, br, b, strong, em, i.

  • Lists<ul> renders bullets, <ol> renders numeric markers.
  • Images<img> loads async from src. Supports object-fit (fill, contain, cover, none, scale-down) and object-position. Changing src skips layout for fast swaps.
  • Form elementsinput, textarea, select, button render with default styles and built-in interaction (focus ring, selection, placeholder, blinking cursor, select dropdown).

Fonts

Built-in Fonts

Font Family Weights Styles
inter (default) 400, 700 normal, italic
jetbrains mono 400, 700 normal, italic (400 only)
icons 400 normal

Generic families map to built-ins: sans-serif/system-uiinter, monospacejetbrains mono.

Custom Asset Fonts

Import a .ttf, .otf, or .woff as a project asset and reference it by filename (without extension) in CSS:

.pixel {
  font-family: slkscr;
  font-size: 20px;
}

Fonts load asynchronously — text renders in Inter initially, then swaps once loaded.

Events & Interaction

To enable interaction, register the panel's hitbox for raycasting:

world.addRaycastable(ui.hitbox);

The engine converts 3D raycasts into 2D pixel coordinates and dispatches DOM events through the element tree. :hover rules, text input, and wheel scrolling on overflow: auto/scroll elements all work automatically.

XR input is fully supported — controller rays and hand tracking drive the same event pipeline, so hover, click, and scrolling behave the same in VR/AR as they do with a mouse.

XR Haptic Feedback

Use the custom xr-haptic CSS property to trigger controller vibration when hovering over elements. This is typically used in :hover rules so users feel a subtle pulse as they move between interactive elements.

xr-haptic: <intensity> <duration>;
  • intensity — Vibration strength from 0 to 1.0
  • duration — Length in milliseconds
.btn:hover {
  background: #3c3c42;
  xr-haptic: 0.2 20;
}

Set xr-haptic: none on a child element to opt out of a parent's haptic. The property bubbles up the tree, so setting it on a container applies to all children:

.toolbar:hover {
  xr-haptic: 0.15 15;
}
.toolbar .no-haptic:hover {
  xr-haptic: none;
}

It can also be set via inline styles:

el.style.xrHaptic = "0.3 25";

For click haptics, use e.hand in the event handler to pulse the correct controller:

btn.onclick = (e) => {
  if (e.hand) controls[e.hand].pulse(1.0, 30);
};

On desktop (no XR), xr-haptic is ignored and e.hand is null.

Limitations

Not Supported

  • Animations / transitions (@keyframes, transition)
  • Media queries (@media)
  • Pseudo-elements (::before, ::after)
  • Advanced pseudo-classes (:nth-child, :not, :focus, :active, etc.)
  • CSS transform
  • Filters (blur(), brightness(), etc.)
  • Standalone opacity — use rgba() colors instead
  • background-image — use gradients or <img> elements
  • Text decoration (underline, strikethrough)
  • text-overflow: ellipsis
  • float
  • Advanced attribute selectors (~=, ^=, $=, *=)
  • Non-solid border styles (dashed/dotted/double render as solid)
  • @font-face — use asset fonts instead

Partial Support

  • position: fixed — treated as absolute
  • Scrolling — no momentum/inertia, no horizontal scroll, no visible scrollbar track