Getting Started
One component model across frameworks
The component contract comes first
A Looma definition declares the component tag, native light-DOM root, inputs, methods, events, slots, and controller behavior. HTML Next is the canonical form. Vue, React, Preact, Svelte and Angular project that same contract into its own lifecycle; it does not invent a competing API.
- Author declaratively
Semantic content exists before JavaScript and remains visible to the platform. - Lower to a native root
The runtime attaches the declared state and controller without a shadow-root bridge. - Project into a host
Framework adapters own lifecycle integration while preserving the same public contract.
The same component, expressed for your host
HTML Next is selected by default. Framework examples import the same definitions through
@threadlabs/looma; the live result exercises the same Looma definition.
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<ui-stack
gap="m"
>
<h2>Account</h2>
<ui-button
variant="solid"
>Save</ui-button>
</ui-stack><script setup>
import { UiButton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="m"
>
<h2>Account</h2>
<UiButton
variant="solid"
>Save</UiButton>
</UiStack>
</template>import { UiButton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<h2>Account</h2>
<UiButton
variant="solid"
>Save</UiButton>
</UiStack>;
}/** @jsxImportSource preact */
import { UiButton, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<h2>Account</h2>
<UiButton
variant="solid"
>Save</UiButton>
</UiStack>;
}<script>
import { UiButton, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="m"
>
<h2>Account</h2>
<UiButton
variant="solid"
>Save</UiButton>
</UiStack>import { Component } from "@angular/core";
import { UiButton, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiButton],
template: `
<div
ui-stack
[gap]="'m'"
>
<h2>Account</h2>
<ng-template
ui-button
[variant]="'solid'"
>Save</ng-template>
</div>
`,
})
export default class Example {}Install Looma
Looma's HTML definitions are the source for Vue, React, Preact, Svelte and Angular components. In an existing Vite project, install Looma and the HTML Next Vite adapter:
pnpm add @threadlabs/looma
pnpm add -D @nextwebwg/html-next-unpluginLooma installs its HTML Next runtime dependency automatically. Configure the adapter before your framework's Vite plugin, then import components from @threadlabs/looma. The adapter generates the framework code and types in your project.
Framework conversion requires Vite adapter alpha.79 or later, which includes Vue server preparation for date controls. Looma declares its runtime minimum in its package dependencies; both packages admit later releases beyond 1.0.
Vue
If your project does not already have Vue tooling, install it:
pnpm add vue
pnpm add -D @vitejs/plugin-vue vite vue-tsc typescript// vite.config.ts
import { defineConfig } from "vite";
import htmlNext from "@nextwebwg/html-next-unplugin/vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [htmlNext({ target: "vue" }), vue()],
});<script setup>
import { UiButton, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack gap="m">
<h1>Account</h1>
<UiButton variant="solid">Save</UiButton>
</UiStack>
</template>React
If your project does not already have React tooling, install it:
pnpm add react react-dom
pnpm add -D @vitejs/plugin-react vite typescript @types/react @types/react-dom// vite.config.ts
import { defineConfig } from "vite";
import htmlNext from "@nextwebwg/html-next-unplugin/vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [htmlNext({ target: "react" }), react()],
});The component names retain their prefix:
import { UiButton, UiStack } from "@threadlabs/looma";
export default function Account() {
return <UiStack gap="m"><h1>Account</h1><UiButton variant="solid">Save</UiButton></UiStack>;
}Preact
For Preact 11.1.0 or later, install its Vite preset and JSX types:
pnpm add preact
pnpm add -D @preact/preset-vite vite typescript// vite.config.ts
import { defineConfig } from "vite";
import htmlNext from "@nextwebwg/html-next-unplugin/vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [htmlNext({ target: "preact" }), preact()],
});/** @jsxImportSource preact */
import { UiButton, UiStack } from "@threadlabs/looma";
export default function Account() {
return <UiStack gap="m"><h1>Account</h1><UiButton variant="solid">Save</UiButton></UiStack>;
}Use Vite adapter alpha.80 or later. Set jsx to react-jsx and jsxImportSource to preact in your TypeScript configuration. Run html-next-sync before tsc --noEmit so library imports have generated component types. For SSR, install preact-render-to-string.
Svelte
If your project does not already have Svelte tooling, install it:
pnpm add svelte@5
pnpm add -D @sveltejs/vite-plugin-svelte vite svelte-check typescript// vite.config.ts
import { defineConfig } from "vite";
import htmlNext from "@nextwebwg/html-next-unplugin/vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
export default defineConfig({
plugins: [htmlNext({ target: "svelte" }), svelte()],
});<script>
import { UiButton, UiStack } from "@threadlabs/looma";
</script>
<UiStack gap="m"><h1>Account</h1><UiButton variant="solid">Save</UiButton></UiStack>Server rendering date controls
Date controls prepare their calendar data before server rendering. With Vite adapter alpha.82 or later, React and Preact applications import renderPrepared from @threadlabs/looma and await renderPrepared(<App />). Svelte applications use await renderPrepared(App, { props }). This returns the application's HTML with prepared data for browser hydration. Use the framework's normal hydration API.
Vue and Angular applications use their ordinary server renderer; their generated components wait for preparation during that render.
Angular
HTML Next's Angular target uses native attribute selectors. Import the generated classes in your standalone component. Fixed roots use their native element, such as <div ui-stack>; roots that can change element type use <ng-template ui-button>. Angular inserts the chosen native root and keeps the component's state when that root changes. Recursive components follow the source data.
Add the adapter alongside your Angular AOT compiler in Vite:
import htmlNext from "@nextwebwg/html-next-unplugin/vite";
// Add this to the application's existing Vite plugins.
htmlNext({ target: "angular" });Use adapter alpha.82 or later. Before standalone Angular template checking, run html-next-sync --target angular and extend src/html-next.angular.json from the application's TypeScript configuration. Keep the Angular compiler's normal options, including strictTemplates.
Default content goes inside the component's native element or structural template. For named slots on a structural component, declare Angular templates and pass their references through [slots]. See Angular setup and examples and the converter guide.
Styles and type checking
Import tokens and one theme once in your application entry:
import "@threadlabs/looma/tokens.css";
import "@threadlabs/looma/theme-light.css";The framework compiler includes each component's styles. A source consumer does
not import @threadlabs/looma/vue.css.
Applications that start touch-input tracking explicitly import trackInputModality
from @threadlabs/looma/input-modality and call it with their document during client
setup. This replaces its former Vue-barrel export; its behavior is unchanged.
The adapter discovers Looma through its html-next export condition. An HTML
folder export names ui-button as UiButton, ui-site-header as UiSiteHeader,
and ui-editor-toolbar as UiEditorToolbar. These names apply in every framework target.
Vite prepares framework declarations automatically. Run html-next-sync before
standalone vue-tsc, tsc, or svelte-check, and include src/html-next.d.ts in
your TypeScript project. Generated files under node_modules/.html-next and the
ambient declaration file may be ignored by Git.
Named slots use Vue's <template #brand>, React and Preact's slots={{ brand: <a>…</a> }},
Svelte snippets passed through slots={{ brand }}, or Angular template references on structural components. Default content uses the
framework's normal children syntax. Declared events retain their native
CustomEvent payloads; read event.detail.
HTML Next pages
Link the definitions used by the page:
<link rel="component" href="@threadlabs/looma/components/ui-button/ui-button.html">
<ui-button variant="solid">Save</ui-button>An HTML Next page compiler resolves the package resource. A browser-only page
needs a browser-resolvable URL and the HTML Next browser runtime. The .html
file is a component definition, not an ordinary JavaScript component import.
Editors
Vue and Tiptap are optional to Looma as a whole. Install only the framework and editor peers used by your application.
The document editor (UiEditor), short field (UiCompactEditor), and semantic JSON renderer (UiCompactContent) are HTML Next components. Import them through the same Vite plugin as the other controls. Install @tiptap/core and @tiptap/pm for editing; there is no Tiptap Vue dependency.
Use the supported Tiptap 2 peers:
pnpm add '@tiptap/core@>=2 <3' '@tiptap/pm@>=2 <3'Pass JSON through content, handle change.detail.content, and answer the host integration requests:
<script setup lang="ts">
import { ref } from "vue";
import {
UiEditor,
} from "@threadlabs/looma";
import type { LoomaImageUploader, LoomaMentionQuery } from "@threadlabs/looma/editor";
const content = ref({ type: "doc", content: [{ type: "paragraph" }] });
const findPeople: LoomaMentionQuery = async ({ query, limit }, { signal }) => {
const response = await fetch(
`/api/people?q=${encodeURIComponent(query)}&limit=${limit}`,
{ signal },
);
return (await response.json()).people;
};
const uploadImage: LoomaImageUploader = async ({ file }, { signal }) => ({
src: await upload(file, { signal }),
alt: file.name,
});
</script>
<template>
<UiEditor
:content="content"
@change="content = $event.detail.content"
:mention-query="findPeople"
:mention-limit="8"
:image-upload="uploadImage"
/>
</template>To add only Looma table behavior to an existing Tiptap editor:
import { Editor } from "@tiptap/core";
import { LoomaTableKit } from "@threadlabs/looma/editor/extensions";
const editor = new Editor({ extensions: [LoomaTableKit] });Know the boundary
@threadlabs/loomais the complete R1 public package; supported capabilities live at its explicit subpaths.- Every published component contract lowers to its declared native light-DOM root. No custom-element registry or shadow-root implementation is part of the public model.
- Vue, React, Preact, Svelte and Angular use the HTML Next Vite adapter; framework compilation belongs to the consumer.
UiEditorowns its Tiptap lifecycle, formatting controls, slash commands, bounded mention suggestions, focus behavior, image insertion, and table editing.- Hosts own persistence, the authorized people-directory query, upload transport, collaboration, presence, workspace/page concepts, and app-specific commands.
Read the Release 1 support and limitations before adopting Looma, then use the component pages for exact markup and API contracts.