Develop

UIKitML (Authoring)

Updated: Sep 4, 2026
UIKitML is a strict HTML- and CSS-like language for spatial UI. IWSDK parses .uikitml source with @drawcall/uikitml and instantiates live @pmndrs/uikit components in the browser.

Author a document

The default IWSDK configuration includes HTML-like elements, Horizon components, and Lucide icons.
<style>
  .panel-root {
    flex-direction: column;
    gap: 8;
    padding: 16;
    background-color: #18181b;
  }

  .action:hover {
    background-color: #3f3f46;
  }
</style>

<Panel class="panel-root">
  <h1>Settings</h1>
  <button id="save-button" class="action">
    <ButtonIcon><Settings /></ButtonIcon>
    Save
  </button>
</Panel>
Names are case-sensitive. Unsupported tags and properties are parser errors. Use kebab-case CSS properties in inline styles and <style> blocks.

Store the source file

Put the source under public/ui.
public/
└── ui/
    └── settings.uikitml
Vite serves the file during development and copies it unchanged into the production output. Do not create a compiled JSON counterpart.

Register and instantiate the asset

const assets = {
  settings: {
    type: AssetType.UIKitML,
    url: '/ui/settings.uikitml',
  },
} satisfies AssetManifest;

const world = await World.create(container, {
  assets,
  features: { spatialUI: true },
});

const settings = await world.assets.instantiate<UIKitMLAsset>('settings');
const entity = world.createTransformEntity(settings);
The loader fetches, parses, and instantiates the source. It returns a UIKitMLAsset, which is a placeable Three.js Group containing its UIKitDocument.

Query required controls

Use stable IDs for controls referenced by application code.
const settings = world.requireSceneObject<UIKitMLAsset>('settings-panel');
const saveButton = settings.requireElementById('save-button');

saveButton.addEventListener('click', saveSettings);
Before handling these events, add RayInteractable to the panel’s owning entity for browser-pointer and XR-ray input, PokeInteractable for touch input, or both.
requireElementById() throws when a required element is missing. Use getElementById() when absence is valid.
Arbitrary data-* attributes are not UIKit component properties. Store associated application values in TypeScript or ECS data.

Load TTF fonts

Declare each font weight with @font-face.
<style>
  @font-face {
    font-family: 'Brand Sans';
    src: url('./fonts/BrandSans-Regular.ttf');
    font-weight: 400;
  }

  @font-face {
    font-family: 'Brand Sans';
    src: url('./fonts/BrandSans-Bold.ttf') format('truetype');
    font-weight: 700;
  }

  .title {
    font-family: 'Brand Sans';
    font-weight: 700;
  }
</style>

<h1 class="title">Welcome</h1>
IWSDK resolves relative font URLs from the UIKitML document URL and loads the requested TTF weight at runtime.

Add custom components

Pass typed component sets through the spatial UI feature configuration.
const world = await World.create(container, {
  features: {
    spatialUI: {
      kit: 'horizon',
      componentSets: [applicationComponents],
    },
  },
});
The kit value is horizon by default; default selects the base set. Custom definitions participate in parser validation and runtime instantiation.

Handle errors

  • Fetch failures identify the UIKitML URL and HTTP status.
  • Parse failures include source location information.
  • Unknown elements and unsupported properties fail validation.
  • A failed asset rejects instantiate() or the scene load that references it.