google/uiblocks
>- 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.
npx skills add https://github.com/google/xrblocks --skill uiblocks
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. strokeWidth not
> borderWidth, hex colors not rgba()); guessing CSS-like names is the most common cause of
> broken UI.
xb.SpatialPanelThe 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 |
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:
> [!NOTE]
> Adjust the relative paths of uiblocks and xrblocks in the import map for the depth of your
> implementation folder relative to the repository root.
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 renderer
xb.add(new CustomScript());
await xb.init(options);
}
document.addEventListener('DOMContentLoaded', start);
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(): voidA UICard is the physical spatial canvas in 3D space. It is grabbable by default if it has
behaviors attached.
position: THREE.Vector3, rotation: THREE.Quaternion.sizeX and sizeY (meters).pixelSize): physical size of exactly 1 flexbox pixel (default 0.002 m).anchorX ('left'|'right'|'center'|number), anchorY('bottom'|'top'|'center'|number) in local space.
UICard inherits @pmndrs/uikit's Container (via ManipulationPanel),so it supports flexDirection, justifyContent, alignItems, gap, padding, etc.
> [!TIP]
> For card mounting, sizing, anchoring, and density configs: samples/basic/cards/.
uiblocks primitives derive from @pmndrs/uikit classes, enhanced with spatial features.
Sources: core source directory.
Generic layout container (like an HTML <div>) — the primary element for grouping, styling,
layout, and capturing interactions.
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/.
strokeWidth, strokeColor, strokeAlign: 'inside'|'outside'|'center'.cornerRadius.dropShadowColor (color or gradient), dropShadowBlur,dropShadowPosition ([x,y] or THREE.Vector2), dropShadowSpread, dropShadowFalloff.
innerShadowColor, innerShadowBlur, innerShadowPosition,innerShadowSpread, innerShadowFalloff.
> [!IMPORTANT]
> Anti-pattern: never use CSS-like borderWidth / borderColor on UIPanel — they force a
> rigid rectangular border that ignores cornerRadius, producing sharp, non-rounded edges.
> Correct: use strokeWidth / strokeColor, which follow corner clipping.
Fills (fillColor) and strokes (strokeColor) accept multi-stop gradient objects:
gradientType: 'linear' | 'radial', rotation (degrees, linear), stops: [{position, color}].
→ samples/basic/panels/.
onHoverEnter, onHoverExit.onClick (fires when the controller trigger/select finishes on the panel).true to mark the event handled and suppress downstreamfallback clicks. → samples/basic/interactions/.
Multi-channel signed distance field (MSDF) text.
fontSize (px), fontWeight ('normal'|'bold'|number), color, textAlign('left'|'right'|'center'), maxWidth, lineHeight.
setText(text), setFontSize(size), setColor(color), setOpacity(opacity).Static 2D images/textures.
setSrc(src: string | THREE.Texture), setColor(color), setOpacity(opacity),setBorderRadius(radius).
Reactive Material Design vector icon loader (queries CDN repositories).
icon (snake_case, e.g. 'star'), iconStyle ('outlined'|'rounded'|'sharp'),iconWeight (100–700), iconFill (0 or 1).
setIcon(icon), setIconStyle(style), setIconWeight(weight),setIconFill(fill), setColor(color).
> [!TIP]
> Text/image/icon examples: samples/basic/elements/.
Behaviors extend UICardBehavior and attach to cards via the behaviors: [...] array on
createCard, to manage positioning relative to the camera, controllers, or other objects.
offset: THREE.Vector3,posLerp (default 0.1), rotLerp (default 0.1).
mode: 'cylindrical' | 'spherical'(cylindrical locks to Y), lerpFactor.
draggable,faceCamera, manipulationMargin (px), manipulationCornerRadius.
THREE.Object3D. target,mode: 'position' | 'rotation' | 'pose', positionOffset, rotationOffset.
showAnimation: 'scale',hideAnimation: 'scale', duration (s).
> [!TIP]
> Attaching/configuring behaviors: samples/basic/behaviors/.
uiCore.createCard() defaults the root box to 200 layout px with alignItems: 'stretch'.
maxWidth < 200 aligns to the left edge, not center.width: 'auto' and alignItems: 'center' to createCard() to shrink-wrap andcenter on the card pivot.
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.
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.
UICard (width: 'auto', alignItems: 'center').UICard per section — it is costly and causes spatialdrift. Use a single card.
flexDirection: 'row',gap: 20, padding: 40) and add child UIPanels as sections.
flexGrow or percentage width/height.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: **one UICard per spatial pivot; express structure with nested UIPanels +
> flexbox** (§5.4). Multiple cards drift apart and depth-sort against each other; one card stays
> crisp and coherent.
Elegance at a distance comes from consistency. Fix a small token system up front and reuse it:
pixelSize) — meters per layout pixel of a card (default 0.002). Set it onceper 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.
fontSize steps (e.g. 40 / 28 / 20) plus fontWeight for emphasis.gap / padding / margin (e.g. 8 →8/16/24/40). Consistent rhythm is what makes a layout feel designed.
cornerRadius values, reused on the card and nested panels.Gradients for depth, sparingly; strokes for emphasis.
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.
fontSize: 20–28 with a sensiblepixelSize reads comfortably. Verify in the simulator, then on device.
HeadLeashBehavior with gentle posLerp/rotLerp (~0.1) sothe panel trails the gaze smoothly instead of snapping. For world-placed panels, a
BillboardBehavior ('cylindrical' keeps text upright) keeps them legible from any angle.
xb.user.safeSpaceRadius.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:
dropShadowColor/Blur/Spread/Position) lift a surface above its background.strokeWidth/strokeColor, never borderWidth) separate adjacent surfaces andemphasize the active element.
position.z nudges (≈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.
In AR the panel sits over the unpredictable real world. To stay elegant _and_ readable:
fillColor (high alpha) for surfaces carrying text — translucent glasslooks great on a clean wall and unreadable over clutter.
text + one text-muted over many greys.ToggleAnimationBehavior (showAnimation: 'scale', duration ~0.2 s) makespanels appear/dismiss with a quick scale instead of popping.
onHoverEnter /onHoverExit (brighten fillColor, grow strokeWidth) and act on onClick.
lerp on leash/billboard readsas "organic"; everything bouncing reads as noise.
ManipulationBehavior({draggable: true, faceCamera: true}). Give it amanipulationMargin (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.
ObjectAnchorBehavior({target, mode: 'pose'}) forlabels and contextual menus; pair with a BillboardBehavior so the label stays readable. Anchor
ManipulationBehavior combine (anchored, still nudgeable).Header / scrollable body / footer-actions in a single card: tokens applied, elevation via stroke
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 length
position: 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/panels/, and
> samples/basic/behaviors/ — they show density, gradients,
> shadows, and behavior combinations end-to-end.
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).
uiblocks bug/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."_
Clicks/selections/hovers not triggering. Self-check:
init(), verify xb.core.input.raycaster.sortFunction = raycastSortFunctionis assigned.
main.js / index.html), verify options.enableUI() andoptions.uikit.enable(uikit) are called.
pointerEvents: 'none' is not set on the target.UICard for overlapping siblings physically masking theinteractive element.
Shadows / strokes / corners not rendering. Self-check:
strokeWidth/strokeColor are used instead of borderWidth/borderColor.dropShadowBlur, innerShadowBlur) are non-zero.transformTranslateZ or position.z = 0.001) torule out Z-fighting.
'#ffffff'/'#fff') or THREE.Color — not rgba(...) /hsla(...).
them to hex or THREE.Color."
Elements squished to zero, overflowing, or misaligned. Self-check:
UICard dimensions (sizeX/sizeY or width/height).width: 'auto' + alignItems: 'center' to avoid default stretching.pixelSize so child pixel measurements map correctly.pixelSize; expectedchild pixel dims / padding / margins / alignment; any design spec (e.g. Figma) to match.
Take google/uiblocks from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.