Loomguides

Troubleshooting

Blank previews, missing globals, React duplication and resolution errors — what each one means.

Every symptom below was reproduced on a real project, ordered roughly by how often it comes up.

Quick index

SymptomCauseFix
Blank page, [loom] nothing mounted into #loom-root after 2sEntry exports instead of mountingCall createRoot().render(...) at top level
ReferenceError: UDim2 is not defined in a built pageA page whose entry the plugin could not seeimport "@loom-dev/preview/globals" first in the entry
Invalid hook call / two React copiesStale optimizer cache, or React 19 installedrm -rf node_modules/.vite; pin React 18
Failed to resolve entry for package "@loom-dev/layout"Plugin imported by file path, not bare specifierImport @loom-dev/preview/vite
Cannot find module '.../dist/vite.js' when loading the configlink:ed checkout that was never builtpnpm build:packages in the checkout
no index.html and no client entry foundNeither a conventional entry nor an index.html under the rootPass entry, or targets for a gallery
no targets matched … — nothing to buildGlob missed, or unsupported glob syntaxOnly **, **/ and * are supported
Package "…" only provides a Lua/Luau runtimeA declaration-only roblox-ts packageAdd a shims entry
RollupError: Expected ';', '}' or <eof> in a .luau file, build onlyThe same thing, reached by a target the dev server never loadedAs above
Renders, but looks wrongUnimplemented propertyCheck Supported instances and properties
Text or buttons spill out of their containerContent wider than the parent allowsExpected — AutomaticSize is bounded and the engine overflows rather than widening
Runs off the edge on a phone, or a narrow window reflowsExpected — the scene lays out against the real viewport, as the engine does?base=960 keeps the wide composition and scales it to fit; see Previews on a phone
no face is loaded for the Roblox font family "…"A family loom names but cannot redistribute — Gotham, BuilderSans, …Register your own copy
A list orders itself alphabeticallySortOrder defaults to Name, as it does in StudioSet SortOrder={Enum.SortOrder.LayoutOrder}
You cannot tell what a preview is doing at all—Open the debug panel: ?debug=1, the sidebar’s debug button, or Ctrl+Alt+D

Blank page, nothing mounted into #loom-root after 2s

[loom] nothing mounted into #loom-root after 2s — does your entry call
createRoot().render(<App />) at the top level?

The globals installer sets a two-second timer and checks whether #loom-root has any children, so this means loom loaded fine and your entry mounted nothing. Almost always the entry exports a component instead of rendering one — loom never calls your component for you:

src/main.client.tsx
import { createRoot } from "@rbxts/react-roblox";
import { App } from "./App";
createRoot().render(<App />); // this line is the whole contract

The other cause is loom picking a different entry than you expected. It takes the first match from a fixed candidate list, so a project with both src/main.client.tsx and src/main.tsx uses the former. Check the <script src> in the served page, and pass loomPreview({ entry }) to override.

ReferenceError: UDim2 is not defined

The Roblox datatype globals were never installed. Under the dev server, a plugin injects them as a script tag. Under vite build, the plugin prepends the globals import to the page’s entry modules, since a tag injected by transformIndexHtml never joins the bundle.

In a page you built, the plugin could not see your entry. Likely a build.rollupOptions.input of your own, or an entry pulled in by something other than a <script type="module" src>. Add the import yourself, first in the entry:

import "@loom-dev/preview/globals";

Under the dev server, something is loading before the injected script — most likely an inline <script type="module"> in your index.html. Move that logic into the entry module.

Warning: Invalid hook call / “more than one copy of React”

Two React instances are in the graph. The plugin aliases bare react and both JSX runtimes to one absolute path, so the adapter, the reconciler and your components converge. When it happens anyway, one of three things is true:

  1. A stale optimizer cache, where Vite re-optimized mid-session and served a mix of old and new pre-bundles. rm -rf node_modules/.vite and restart.
  2. React 19 is installed. The adapter’s react-reconciler@0.29 reads React 18 internals that React 19 renamed. Pin React 18 in the preview project.
  3. You merged loomPreview() into an app that has its own React. The aliases are global to the Vite config. Give the preview its own Vite project — see Vite integration.

Failed to resolve entry for package "@loom-dev/layout"

You imported the plugin by file path instead of by bare specifier. The plugin resolves the modules it aliases relative to its own location, and bundling it into your config moves that anchor.

import { loomPreview } from "@loom-dev/preview/vite"; // always this

The config fails to load with Cannot find module '.../dist/vite.js'

A link:ed source checkout that has never been built. Run pnpm build:packages in the checkout. To work against unbuilt sources, vite --configLoader runner loads the config through Vite’s module runner instead of pre-bundling it.

loom: no index.html and no client entry found

The directory has neither an index.html nor any of the eight recognized entry paths — usually you aimed at a workspace root rather than an app. Point deeper, name the entry with loomPreview({ entry }) (the CLI has no flag for it), or switch to gallery mode with --targets.

no targets matched ... — nothing to build

loom build found no files matching the glob. A bare directory expands to <dir>/**/*.loom.tsx, and the matcher supports only **, **/ and * — no braces, no ?, no negation. The walk also skips node_modules and dot-directories.

A scene renders but looks wrong

Check the property against Supported instances and properties before assuming a loom bug. A surprising number of “wrong render” reports are a prop that is typed, settable, and read by nothing — UIGradient.Offset, ScrollBarThickness. The one image combination loom cannot reproduce is tiling a sprite window, and it warns rather than pretending.

A preview is a high-fidelity approximation, not a Studio replacement.

Text is a different size, or wraps differently, than on your colleague’s machine

loom: no face is loaded for the Roblox font family "Gotham" — its text is painted
and measured in the system fallback instead, …

A browser ships none of Roblox’s typefaces. AutomaticSize and TextWrapped are driven by measuring glyph widths, so a missing face moves the layout, not just the paint. The preview loads 28 of the engine’s families itself, with no import and no configuration. The warning survives for the ones loom cannot redistribute: Gotham, BuilderSans and six more. Register your own copy with registerFont. Both halves are in Fonts and text metrics.

The debug panel’s fonts section answers this directly: it lists every typeface the scene resolved to and whether the browser really loaded it.

Content spills out of its container

An AutomaticSize object grows only up to the room its parent leaves, which is what Roblox does. Content with an irreducible minimum overflows the box instead of widening it: a long unbreakable word, a row of buttons that will not fit. That overflow is the same one Studio shows.

If a preview overflows and Studio does not, the layout is genuinely too small for its content at that viewport. Give the parent more room, or let the text wrap.

A target throws and the page still works

By design. The gallery shell is plain DOM with a React error boundary around each target. Import failures, bad preview exports and render throws each land in an inline red panel with a stack, while the sidebar stays usable. Switching targets clears it.