Velaguides

Troubleshooting

A symptom-first map of Vela's failure modes, the silent ones especially.

Vela reports most mistakes as diagnostics. This page is for the other situation: something is wrong on screen and the build said nothing.

”A class did nothing”

Sorted by how often each turns out to be the answer.

1. The utility does not belong on that host element. <frame className="text-red-500" /> compiles and emits TextColor3 onto a Frame, which rejects it. The static path warns unsupported-host-utility and writes it anyway. The runtime path drops it, so the same class list can behave differently on the two paths.

2. The class is on a component that does not forward it. <Panel className="p-4" /> hands Panel resolved props and helper children. If Panel does not spread its unrecognized props onto a host element and render children, everything vanishes with no warning. See How it works.

3. The element is on the runtime path and the class value is unchecked. A typo that would warn in a literal is silent in a computed string. A runtime-path element renders as VelaRuntimeHost in the compiled output — see dynamic class names.

4. Your build drops Vela diagnostics entirely. Diagnostics reach you through roblox-ts’s context.addDiagnostic. On a version that does not expose it, every warning disappears. Confirm by compiling className="bg-nope-500" and checking that a warning appears.

5. The editor stayed quiet, but the key is wrong. The editor suppresses unknown-theme-key while your token is still a prefix of a real key. bg-slat shows no squiggle and still warns in the build.

6. transition on a static element. A static element never changes, so it is dropped with transition-without-runtime. Add the hover: or breakpoint rule that gives it something to tween.

”The editor flags a class the build accepts”

The build is the source of truth, so a squiggle the compiler disagrees with is an editor bug. Upgrade.

They genuinely disagree in one direction by design. The per-host rules, reported as unsupported-host-utility, are enforced in the editor and never by the build. That is the first entry on this page.

”A breakpoint or orientation variant never applies”

Check the token’s other conditions — a chained md:touch: needs both to match. Bare thresholds are min-width on the viewport’s X dimension, so a phone-sized viewport matches none of them. A square viewport counts as landscape.

”The layout is wrong”

left-* / top-* are ignored. The parent has a UIListLayout, created by any flex, gap-* or space-*, and a list layout owns its children’s positions. Position an element only under a plain frame.

An element sits outside its parent after right-* or bottom-*. Those set the coordinate from the far edge but do not anchor the element. Its top-left corner lands there. Add the matching origin-*, as in right-2 bottom-2 origin-bottom-right.

justify-* behaves strangely. justify-center and justify-between set two different properties (HorizontalAlignment vs HorizontalFlex), so writing both makes them interact inside the layout solver. Pick one. See layout and sizing.

Spacing is bigger than you asked for. Margins are wrappers that participate in the parent’s list, so they sum with its gap-*. Prefer gap alone for list spacing.

A grid stops laying out when you add gap-*. Still open: gap on a grid element also emits a conflicting UIListLayout. Set CellPadding as a prop and keep gap-* off.

Children shuffle when divide-* is present. Separators are inserted by child position and break under explicit LayoutOrder/order-*. Use source order with dividers.

order-* does nothing. The layout instance is one you wrote yourself, and SortOrder defaults to Name, which sorts alphabetically and ignores LayoutOrder. Set it to LayoutOrder. Vela’s own layouts set it for you.

”The colors are wrong”

An element that used to be visible disappeared. It relied on Roblox’s default grey background, and preflight starts every classed host element at BackgroundTransparency = 1. Give it the bg-* it was implicitly using, or set preflight: false. Elements with no className, and components, are never preflighted.

A gradient renders as a flat fill. One of its stops is transparent, which is silently dropped. This is the one transparency case that produces no diagnostic.

bg-* seems ignored next to a gradient. By design: a gradient forces BackgroundColor3 to white so the stops read true, overriding any bg-* regardless of order.

A theme colour you defined reports unknown-theme-key. Two suspects. A top-level theme.colors silently discards theme.extend.colors along with every built-in palette. And discovery walks up from each source file, so a stray vela.config.ts in a subdirectory shadows the root one. See merge semantics.

opacity-* fades a transparency you set yourself. Expected when the fade comes from an ancestor component. The alpha arrives as React context, and a consumer cannot tell your BackgroundTransparency prop from one Vela lowered.

Overlapping siblings darken where they overlap. opacity-* fades each instance rather than compositing the group. Put them in a canvasgroup and fade that.

A themed colour works statically but not on a dynamically-classed element. The runtime path re-parses theme values from text. It accepts exactly Color3.fromRGB(r, g, b) and new UDim(a, b) with numeric literals. Anything else silently degrades. See theming.

”Everything got bigger or smaller after upgrading to 0.12.0”

Expected. Every pixel offset is a rem unit following the viewport, so a layout tuned by eye at one resolution keeps its proportions there and changes everywhere else.

A screen uniformly off means the viewport you are testing at. Off only at the extremes means the clamp is doing its job, at min 8 and max 64, both configurable. To rule rem out:

vela.config.ts
export default defineConfig({ theme: { rem: { min: 16, max: 16 } } });

Text stopped growing past a point. TextSize is capped at 100, where Roblox stops honoring it.

@rbxts/vela-runtime does not resolve”

The runtime reaches your place as a dependency of vela-rbxts, and pnpm’s default isolated linker does not put a transitive dependency where roblox-ts and Rojo look. Set the linker at the workspace root:

pnpm-workspace.yaml
nodeLinker: hoisted

npm and Yarn hoist by default. A nested .npmrc will not do it — pnpm reads nodeLinker only from the workspace root.

”A margin renders unspaced under Vide, with a warning”

A margin box is an instance above the element, and Vide parents an element as soon as it builds one. The box has to be decided before the element exists. The compiler handles any m-* it can see, in a static token or a branch, and warns on one arriving out of an opaque call. Name the token where the compiler can read it. See the Vide guide.

className is a type error”

The global augmentation only loads if vela-rbxts is imported somewhere in your program. Add the one-line declaration file — and do not name it src/vela-rbxts.d.ts, which makes the import resolve to itself under roblox-ts’s baseUrl. See installation.

”The build fails on the first file with compiler-invocation-failed

If the message is Cannot read properties of undefined (reading 'Latest'), your TypeScript is too new. 7.x moved the API Vela’s host adapter calls. Pin typescript@5.9.3, which the repo tests against. 6.x works too.

Any other compiler-invocation-failed means the native compiler call threw. Check the platform has a prebuilt binary — Windows on ARM and linux arm64 musl do not.

”Nothing is being transformed at all”

  • The file must end in .tsx — a className built in a .ts helper is invisible to Vela.
  • The tsconfig plugin must be vela-rbxts/transformer, the subpath. The root package’s createTransformer is a different function, and passing it produces no transform and no error.
  • The transformer only runs under rbxtsc — it throws under plain tsc.
  • The file filter skips files with no literal className substring and no JSX open tag. Both are toggleable, but on by default.

”The output got bigger than expected”

A file larger than you expect is usually one whose host has to parse a class value: a computed className, or a host taking a spread. That is the only case that keeps the full theme tables in the emit. Writing the branch out instead of interpolating it is often enough to drop them.

Still stuck?

Compile the smallest possible reproduction and read the emitted TSX before the Luau. A static-path element keeps its tag and gains plain props. A runtime-path element becomes VelaRuntimeHost with __vela* props. Which of the two you are looking at answers most questions on this page, and the five rules say how to move between them.