Loomguides

Vite integration

Add loomPreview() to your own Vite project — the dependencies, the zero-config default, the options, your own HTML entry, static builds and gallery mode.

loomPreview() is the whole product. The loom CLI is a thin wrapper around it — a Vite server with configFile: false and this one plugin — so anything the CLI does, your own vite.config.ts gets by calling the plugin yourself, including the parts that used to be CLI-only: the generated page, the client-entry detection, and gallery mode.

Everything below was verified end to end against Vite 6.4.3 and pnpm 11, on a project sitting outside loom’s own workspace.

1. Dependencies

package.json
{
"name": "my-game-ui",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@loom-dev/preview": "^0.11.0",
"react": "^18.3.1",
"vite": "^6.0.0"
}
}

React and Vite are peers of @loom-dev/preview, so they are yours to install. React 18 is the only supported major — the adapter’s react-reconciler@0.29 targets it, and the peer range says so. See Installation for the reasoning.

2. The config

vite.config.ts
import { loomPreview } from "@loom-dev/preview/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [loomPreview()],
});

That is genuinely the whole config, and there is no index.html to write either. vite and vite build now work against a roblox-ts source tree as-is:

  • loomPreview() returns an array of plugins and contributes its own esbuild, optimizeDeps, resolve.alias and server.fs.allow through the config hook, deep-merged with — and overridable by — whatever you write.
  • With no index.html in the project root, it generates one: a full-viewport #loom-root on a #14161a background plus a module script pointing at your client entry, which it finds by walking the roblox-ts conventions (src/main.client.tsx first — the full list is in Your first preview).

See the plugin reference for the exact contributions.

3. Options

Every option is optional; the defaults are what the CLI uses.

vite.config.ts
loomPreview({
entry: "src/ui/boot.tsx", // only if your entry isn't a conventional name
targets: "src/scenes", // gallery mode: glob, directory, list, or true
title: "my-game-ui", // <title> of the generated page
html: false, // opt out of the generated page entirely
})
OptionTypeDefaultEffect
entrystringauto-detectedThe client entry, root-relative (/src/boot.tsx) or relative to the project root. Only needed when none of the conventional names fit.
targetsstring | string[] | trueGallery mode. Same semantics as the CLI’s --targets: a glob, a directory (expanded to <dir>/**/*.loom.tsx), a list, or true for the default glob. No client entry is needed.
titlestring"loom preview" / "loom gallery"<title> of the generated page.
htmlbooleantruefalse keeps the plugin out of the HTML business entirely — no generated page, no entry detection, no Rollup input. Only the module plumbing stays.

4. Writing your own HTML entry

If you do keep an index.html, it needs one thing loom cares about: an element with id="loom-root". If it is missing, createRoot() creates one and appends it to <body>, but then it is unstyled and the preview will not fill the viewport.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>my-game-ui preview</title>
<style>
html, body { margin: 0; height: 100%; background: #14161a; }
#loom-root { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
</style>
</head>
<body>
<div id="loom-root"></div>
<script type="module" src="/src/main.client.tsx"></script>
</body>
</html>

That is exactly what the plugin generates, so the reason to write it yourself is wanting something else in the document — a font link, a wrapper element, your own background.

The entry itself is an ordinary roblox-ts client entry — see Your first preview. At this point vite serves an interactive preview: state updates, Activated handlers and text input all work.

5. vite build produces a static preview

Terminal window
pnpm vite build # → dist/

The build emits a self-contained client-only SPA: your entry, the adapter, the renderer and the WASM layout binary, hostable anywhere. The Roblox datatype globals are part of the bundle — the plugin prepends import ".../globals.ts" to the page’s entry modules, so installGlobals() runs before any module that touches UDim2 evaluates. That applies to the generated page and to your own index.html, whose module scripts it reads out of the file.

Pass targets and the same *.loom.tsx gallery the CLI serves comes up under vite, and vite build emits it as a static, deep-linkable site:

vite.config.ts
export default defineConfig({
plugins: [loomPreview({ targets: "src/scenes" })],
});

Each target becomes its own async chunk, and the built page honors the same ?target= / ?chrome= / ?theme= / ?background= / ?base= URL contract loom build produces — they are the same code path. The target contract itself is covered in Gallery targets.

Next steps

Three things reliably break a working setup once it moves into a real workspace: importing the plugin by file path, pointing it at an unbuilt source checkout, and sharing a Vite config with an app that has its own React. All three are in Advanced Vite setup.

For error strings and what each one actually means, see Troubleshooting.