Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: uiblocks description: >- Build rich spatial user interfaces in XR Blocks apps with the uiblocks addon — flexbox-laid-out 3D cards and panels with gradients, strokes, rounded corners, drop/inner shadows, MSDF text, material icons, images, and spatial behaviors (head-leash, billboard, grab/manipulate, object-anchor, show/hide animations). Use when authoring or debugging UICard / UIPanel / UIText / UIImage / UIIcon UI imported from xrblocks/addons/uiblocks/src (wrapping @pmndrs/uikit
- yoga-layout) — for panels, menus, HUDs, dialogs, or any styled, web-like UI in
WebXR / Android XR. Includes the bootstrap (options.uikit.enable(uikit) + raycastSortFunction), styling/layout rules, behavior config, and a troubleshooting playbook for clicks/styling/sizing failures. (For lightweight panels with no extra deps, prefer core xb.SpatialPanel instead.) Includes a design guide (§6) for UX designers composing complex, elegant, multi-section spatial UI — tokens, spatial comfort, elevation/shadows, passthrough legibility, motion, and grab/anchor behaviors.
UIBlocks Skills Guide
This reference helps developers and AI agents build interactive spatial user interfaces in xrblocks projects. uiblocks wraps the `@pmndrs/uikit` Flexbox yoga-layout engine and Three.js, offering unified layout components, rich styling (gradients, borders, shadows), and spatial behaviors.
Only use APIs documented here or visible in the `src/core/` sources and the`samples/`. uiblocks property names differ from CSS (e.g.strokeWidthnotborderWidth, hex colors notrgba()); guessing CSS-like names is the most common cause ofbroken UI.
0. When to use uiblocks vs. core xb.SpatialPanel
The XR Blocks core ships a lighter UI system (xb.SpatialPanel().addGrid().addRow()...). Choose deliberately — do not mix the two on the same panel, and never import UIPanel/UICard from xrblocks core (they exist only in this addon).
| Use core `xb.SpatialPanel` when... | Use uiblocks when | |
|---|---|---|
| Quick HUD, menu, or debug panel | You need real flexbox layout (rows/columns, grow/shrink, gap) | |
| No extra dependencies wanted | You need gradients, strokes, rounded corners, drop/inner shadows | |
| Simple text + buttons | You want web-like styling fidelity and reusable spatial behaviors |
1. Setup & Bootstrap
1.1 Import map & boilerplate
Configure the HTML import map to load xrblocks and uiblocks alongside their peer dependencies (@pmndrs/uikit, three, yoga-layout, troika, signals). It must match the versions in the canonical bootstrap sample exactly:
- Bootstrap sample: samples/uiblocks/index.html
- Full import-map block & version notes: README.md
[!NOTE]Adjust the relative paths ofuiblocksandxrblocksin the import map for the depth of yourimplementation folder relative to the repository root.
1.2 Minimal quick start
The 80% case: create a UICore in your Script's constructor, wire the raycast sort in init(), then build a UICard → UIPanel → elements. (Full version: samples/uiblocks/index.html.)
import * as uikit from '@pmndrs/uikit';import * as THREE from 'three';import {UICore, UIPanel, UIText, raycastSortFunction} from 'uiblocks';import * as xb from 'xrblocks';class CustomScript extends xb.Script {constructor() {super();this.uiCore = new UICore(this);}init() {// REQUIRED for raycasting against uiblocks to work correctly.if (xb.core.input.raycaster) {xb.core.input.raycaster.sortFunction = raycastSortFunction;}const card = this.uiCore.createCard({name: 'HelloCard',sizeX: 1.0,sizeY: 0.6,position: new THREE.Vector3(0, 1.5, -1),width: 'auto', // shrink-wrap (see §5.1)alignItems: 'center',});const panel = new UIPanel({width: '100%',height: '100%',fillColor: '#1a1a24',cornerRadius: 20,padding: 30,flexDirection: 'column',justifyContent: 'center',alignItems: 'center',});card.add(panel);panel.add(new UIText('Hello World', {fontSize: 32,fontWeight: 'bold',color: 'white',}));}}async function start() {const options = new xb.Options();options.enableUI();options.uikit.enable(uikit); // REQUIRED to register the uikit rendererxb.add(new CustomScript());await xb.init(options);}document.addEventListener('DOMContentLoaded', start);
2. Canvas Mounting & Lifecycle
2.1 UICore
Central entry point for the UI lifecycle. Automatically adds/removes cards from the parent script group. Construct it with the owning Script: new UICore(this).
createCard(config: UICardOutProperties): UICardcreateAdditiveCard(config: UICardOutProperties): AdditiveUICardunregister(card: UICard): voidclear(): void
2.2 UICard
A UICard is the physical spatial canvas in 3D space. It is grabbable by default if it has behaviors attached.
- Position & rotation:
position: THREE.Vector3,rotation: THREE.Quaternion. - Dimensions: absolute bounds
sizeXandsizeY(meters). - Resolution (`pixelSize`): physical size of exactly 1 flexbox pixel (default
0.002m). - Anchors:
anchorX('left'|'right'|'center'|number),anchorY
('bottom'|'top'|'center'|number) in local space.
- Flexbox layout:
UICardinherits@pmndrs/uikit'sContainer(viaManipulationPanel),
so it supports flexDirection, justifyContent, alignItems, gap, padding, etc.
[!TIP]For card mounting, sizing, anchoring, and density configs: samples/basic/cards/.
3. Primitives & Elements API
uiblocks primitives derive from @pmndrs/uikit classes, enhanced with spatial features. Sources: core source directory.
3.1 UIPanel
Generic layout container (like an HTML <div>) — the primary element for grouping, styling, layout, and capturing interactions.
3.1.1 Flexbox layout
Driven by the Yoga engine: flexDirection: 'row' | 'column'; justifyContent (primary axis); alignItems (cross axis); gap / padding / margin; flexGrow, flexShrink, and percent/absolute width/height. → samples/basic/layouts/.
3.1.2 Strokes, corners & shadows
- Strokes (borders):
strokeWidth,strokeColor,strokeAlign: 'inside'|'outside'|'center'. - Corner rounding:
cornerRadius. - Drop shadows:
dropShadowColor(color or gradient),dropShadowBlur,
dropShadowPosition ([x,y] or THREE.Vector2), dropShadowSpread, dropShadowFalloff.
- Inner shadows:
innerShadowColor,innerShadowBlur,innerShadowPosition,
innerShadowSpread, innerShadowFalloff.
[!IMPORTANT]Anti-pattern: never use CSS-likeborderWidth/borderColoronUIPanel— they force arigid rectangular border that ignorescornerRadius, producing sharp, non-rounded edges.Correct: usestrokeWidth/strokeColor, which follow corner clipping.
3.1.3 Linear/radial gradients
Fills (fillColor) and strokes (strokeColor) accept multi-stop gradient objects: gradientType: 'linear' | 'radial', rotation (degrees, linear), stops: [{position, color}]. → samples/basic/panels/.
3.1.4 Interactions & reactivity
- Hover:
onHoverEnter,onHoverExit. - Click/select:
onClick(fires when the controller trigger/select finishes on the panel). - Handler return type: return
trueto mark the event handled and suppress downstream
fallback clicks. → samples/basic/interactions/.
3.2 UIText
Multi-channel signed distance field (MSDF) text.
- Props:
fontSize(px),fontWeight('normal'|'bold'|number),color,textAlign
('left'|'right'|'center'), maxWidth, lineHeight.
- Methods:
setText(text),setFontSize(size),setColor(color),setOpacity(opacity).
3.3 UIImage
Static 2D images/textures.
- Methods:
setSrc(src: string | THREE.Texture),setColor(color),setOpacity(opacity),
setBorderRadius(radius).
3.4 UIIcon
Reactive Material Design vector icon loader (queries CDN repositories).
- Props:
icon(snake_case, e.g.'star'),iconStyle('outlined'|'rounded'|'sharp'),
iconWeight (100–700), iconFill (0 or 1).
- Methods:
setIcon(icon),setIconStyle(style),setIconWeight(weight),
setIconFill(fill), setColor(color).
[!TIP]Text/image/icon examples: samples/basic/elements/.
4. Spatial Behaviors
Behaviors extend UICardBehavior and attach to cards via the behaviors: [...] array on createCard, to manage positioning relative to the camera, controllers, or other objects.
- HeadLeashBehavior — card gently follows the camera.
offset: THREE.Vector3,
posLerp (default 0.1), rotLerp (default 0.1).
- BillboardBehavior — card faces the camera.
mode: 'cylindrical' | 'spherical'
(cylindrical locks to Y), lerpFactor.
- ManipulationBehavior — 3DOF grabbable drag via controller rays.
draggable,
faceCamera, manipulationMargin (px), manipulationCornerRadius.
- ObjectAnchorBehavior — lock pose to another
THREE.Object3D.target,
mode: 'position' | 'rotation' | 'pose', positionOffset, rotationOffset.
- ToggleAnimationBehavior — scale animations on show/hide/toggle.
showAnimation: 'scale',
hideAnimation: 'scale', duration (s).
[!TIP]Attaching/configuring behaviors: samples/basic/behaviors/.
5. Gotchas & Best Practices
5.1 Default sizing & flex centering
uiCore.createCard() defaults the root box to 200 layout px with alignItems: 'stretch'.
- Quirk: a nested child with
maxWidth< 200 aligns to the left edge, not center. - Fix: pass
width: 'auto'andalignItems: 'center'tocreateCard()to shrink-wrap and
center on the card pivot.
5.2 SVG asset color tinting
When tinting a UIImage via color, the SVG source must use pure white (fill="#FFFFFF" / stroke="#FFFFFF"). Hardcoded greys multiply with the overlay and darken the tint.
5.3 Creating buttons
There is no built-in "button" class. Compose one: a UIPanel container (dimensions, background, interaction hooks) with a UIText / UIIcon / UIImage child. See the interactions sample.
5.4 Multi-section layouts
- Typical: one centered section under one
UICard(width: 'auto',alignItems: 'center'). - Complex (sidebar / header / grid under one pivot):
- One canvas: do not spawn a
UICardper section — it is costly and causes spatial
drift. Use a single card.
- Partition with flexbox: set layout on the root card (e.g.
flexDirection: 'row',
gap: 20, padding: 40) and add child UIPanels as sections.
- Proportions: scale sections with
flexGrowor percentagewidth/height.
6. Designing complex & elegant spatial UI
For designers composing rich, multi-section interfaces. Design with a 2D flexbox mindset, composed into 3D space: each UICard is a flat canvas you lay out with UIPanels and flexbox, then place, anchor, and animate as an object in the world.
Golden rule: **oneUICardper spatial pivot; express structure with nestedUIPanels +flexbox** (§5.4). Multiple cards drift apart and depth-sort against each other; one card stayscrisp and coherent.
6.1 Establish a design-token scale
Elegance at a distance comes from consistency. Fix a small token system up front and reuse it:
- Density (`pixelSize`) — meters per layout pixel of a card (default
0.002). Set it once
per card and size all children in layout pixels against it. Lower = crisper/denser (text-heavy panels); higher = chunkier (glanceable HUDs). Keep it constant across a screen so type and spacing read uniformly.
- Type scale — a few
fontSizesteps (e.g. 40 / 28 / 20) plusfontWeightfor emphasis. - Spacing scale — multiples of one base unit for
gap/padding/margin(e.g. 8 →
8/16/24/40). Consistent rhythm is what makes a layout feel designed.
- Shape scale — one or two
cornerRadiusvalues, reused on the card and nested panels. - Palette — a restrained hex set (surface, surface-variant, accent, text, text-muted).
Gradients for depth, sparingly; strokes for emphasis.
6.2 Place for comfort and readability
- Anchor primary panels near eye height and arm's length:
position.set(0, xb.user.height, -xb.user.panelDistance). Keep the whole card within a relaxed field of view — wide dashboards should curve toward the user, not force head-turning.
- Size text for distance: at ~1.5–1.75 m, body text around
fontSize: 20–28with a sensible
pixelSize reads comfortably. Verify in the simulator, then on device.
- For persistent HUDs, attach a
HeadLeashBehaviorwith gentleposLerp/rotLerp(~0.1) so
the panel trails the gaze smoothly instead of snapping. For world-placed panels, a BillboardBehavior ('cylindrical' keeps text upright) keeps them legible from any angle.
- Don't spawn panels inside
xb.user.safeSpaceRadius.
6.3 Express hierarchy with elevation, not big Z gaps
Large literal Z offsets between sibling surfaces cause uncomfortable parallax and depth-fighting. Convey layering the way good 2D material design does — with light and edges:
- Drop shadows (
dropShadowColor/Blur/Spread/Position) lift a surface above its background. - Inner shadows carve recessed wells (input fields, track grooves).
- Strokes (
strokeWidth/strokeColor, neverborderWidth) separate adjacent surfaces and
emphasize the active element.
- Tiny
position.znudges (≈0.001) only to resolve coplanar z-fighting — not for visual depth.
A surface reads cleanly as fill → stroke → shadow, with cornerRadius shared so shadows follow the corners.
6.4 Stay legible over passthrough
In AR the panel sits over the unpredictable real world. To stay elegant _and_ readable:
- Use sufficiently opaque
fillColor(high alpha) for surfaces carrying text — translucent glass
looks great on a clean wall and unreadable over clutter.
- Add a subtle drop shadow and/or stroke so the card edge separates from any background.
- Keep strong text/background contrast; prefer one
text+ onetext-mutedover many greys. - Gradient fills add depth, but check contrast at both ends of the gradient.
6.5 Make motion and state purposeful
- Enter/exit: a
ToggleAnimationBehavior(showAnimation: 'scale',duration~0.2 s) makes
panels appear/dismiss with a quick scale instead of popping.
- Affordance: give interactive panels visible hover/press states via
onHoverEnter/
onHoverExit (brighten fillColor, grow strokeWidth) and act on onClick.
- Restraint: animate one or two properties, briefly. Smooth
lerpon leash/billboard reads
as "organic"; everything bouncing reads as noise.
6.6 Grabbing and contextual anchoring
- Make a card movable with
ManipulationBehavior({draggable: true, faceCamera: true}). Give it a
manipulationMargin (px) so users grab the frame without hitting inner controls, and match manipulationCornerRadius to the card's cornerRadius. A title/header row doubles as a natural grab handle.
- Attach UI to a real or virtual object with
ObjectAnchorBehavior({target, mode: 'pose'})for
labels and contextual menus; pair with a BillboardBehavior so the label stays readable. Anchor
ManipulationBehaviorcombine (anchored, still nudgeable).
6.7 Worked example — an elegant settings card
Header / scrollable body / footer-actions in a single card: tokens applied, elevation via stroke
- shadow, draggable, animated in.
import {UICore,UIPanel,UIText,UIIcon,ManipulationBehavior,ToggleAnimationBehavior,} from 'uiblocks';const SURFACE = '#16181d',SURFACE_2 = '#1f232b',ACCENT = '#4285f4',TEXT = '#f5f7fa';const card = this.uiCore.createCard({name: 'Settings',sizeX: 0.9,sizeY: 0.62,pixelSize: 0.0016, // crisp at arm's lengthposition: new THREE.Vector3(0, xb.user.height, -xb.user.panelDistance),width: 'auto',behaviors: [new ManipulationBehavior({draggable: true,faceCamera: true,manipulationMargin: 24,manipulationCornerRadius: 28,}),new ToggleAnimationBehavior({showAnimation: 'scale',hideAnimation: 'scale',duration: 0.18,}),],});// Root surface: shared cornerRadius + stroke + drop shadow = one elevated panel.const root = new UIPanel({width: '100%',height: '100%',flexDirection: 'column',fillColor: SURFACE,cornerRadius: 28,padding: 28,gap: 16,strokeWidth: 1,strokeColor: '#33384a', // subtle light-on-dark edge (6-digit hex is always supported)strokeAlign: 'inside',dropShadowColor: '#000000',dropShadowBlur: 24,dropShadowSpread: 2,});card.add(root);// Header (also the grab handle): icon + title on one row.const header = new UIPanel({width: '100%',flexDirection: 'row',alignItems: 'center',gap: 12,});header.add(new UIIcon({icon: 'settings', iconStyle: 'rounded', color: ACCENT}));header.add(new UIText('Settings', {fontSize: 30, fontWeight: 'bold', color: TEXT}));root.add(header);// Body: a recessed well (inner shadow) that grows to fill the remaining height.const body = new UIPanel({width: '100%',flexGrow: 1,flexDirection: 'column',gap: 10,padding: 16,fillColor: SURFACE_2,cornerRadius: 18,innerShadowColor: '#000000',innerShadowBlur: 12,});body.add(new UIText('Display, audio, and account preferences...', {fontSize: 20,color: '#aab2c0',maxWidth: 480,}));root.add(body);// Footer: actions pushed to the edges with justifyContent.const footer = new UIPanel({width: '100%',flexDirection: 'row',justifyContent: 'space-between',gap: 12,});// Compose each action as a UIPanel + UIText/UIIcon child (§5.3).root.add(footer);
[!TIP]Study the assembled patterns in `samples/basic/cards/`,`samples/basic/behaviors/` — they show density, gradients,shadows, and behavior combinations end-to-end.
7. Troubleshooting & Developer Dialog Guide
When a developer reports UI not rendering, wrong styling, or interactions not firing, first investigate the code (read the files, grep) before asking questions. Only ask for what code cannot reveal (simulator/headset visuals, console logs, design intent).
Dialog rules
- Request screenshots when a visual issue can't be explained from code.
- Escalate library limitations: if the root cause looks like a
uiblocksbug/limitation
(not developer setup), stop debugging, summarize the developer's goal and the suspected gap, and tell them: _"I suspect this is a limitation of the current uiblocks library. Please share the summary below with the core engineering team for support."_
7.1 Interaction & input failures
Clicks/selections/hovers not triggering. Self-check:
- In the Script's
init(), verifyxb.core.input.raycaster.sortFunction = raycastSortFunction
is assigned.
- In bootstrap (
main.js/index.html), verifyoptions.enableUI()and
options.uikit.enable(uikit) are called.
- Ensure
pointerEvents: 'none'is not set on the target. - Check the hierarchy under the
UICardfor overlapping siblings physically masking the
interactive element.
7.2 Styling & render failures
Shadows / strokes / corners not rendering. Self-check:
- Verify
strokeWidth/strokeColorare used instead ofborderWidth/borderColor. - Verify shadow blurs (
dropShadowBlur,innerShadowBlur) are non-zero. - Add small Z offsets on nested elements (e.g.
transformTranslateZorposition.z = 0.001) to
rule out Z-fighting.
- Verify colors are hex strings (
'#ffffff'/'#fff') orTHREE.Color— notrgba(...)/
hsla(...).
- _Ask only if code checks pass_: "Are your colors hex strings? If using rgba/hsla, convert
them to hex or THREE.Color."
7.3 Sizing & flexbox failures
Elements squished to zero, overflowing, or misaligned. Self-check:
- Verify the parent
UICarddimensions (sizeX/sizeYorwidth/height). - Check the card uses
width: 'auto'+alignItems: 'center'to avoid default stretching. - Inspect
pixelSizeso child pixel measurements map correctly.
- _Ask only if code checks pass_: target physical dimensions (m) and
pixelSize; expected
child pixel dims / padding / margins / alignment; any design spec (e.g. Figma) to match.