Facetguides

Troubleshooting

The failures that produce no error — unstyled UI, zero-pixel components, labels in the wrong font — and the ones that do.

Start here:

Terminal window
npx facet-rbxts doctor

It checks the config, the import aliases, the transformer, the theme, which components are installed, whether the tokens they name resolve, and whether the packages underneath them meet the floors those files need. Most of what follows is something doctor would have told you.

Nothing is styled at all

Everything renders as grey Roblox defaults. No error, no warning, and the build succeeded.

The Vela transformer is not registered. Without it, every className a component sets is inert.

tsconfig.json
{
"compilerOptions": {
"plugins": [{ "transform": "vela-rbxts/transformer" }]
}
}

facet init and facet doctor both check for this textually and neither edits the file for you — roblox-ts tsconfigs are routinely JSONC, and a pattern-matched edit that mangles one is worse than a printed snippet.

A component renders zero pixels wide

One axis has no answer. UIPadding does not grow a frame on Roblox — it insets children — and Vela starts a frame at UDim2.new(0, 0, 0, 0).

h-9 px-4 → 0 × 36. Invisible.
h-9 w-fit px-4 → correct.

If the component is a container, check the whole chain rather than the outermost level: a container that hugs its content can only measure children that already know their own size, so one label with an unresolved axis collapses everything above it.

A class compiles but does nothing

Suspect the code path before the class. Every Facet class string comes out of fv(), so it is a computed expression, and Vela resolves those at runtime rather than at compile time. That runtime path implemented a strict subset of the static lowering for a long time:

Below VelaThese silently do nothing
0.7.0flex-*, items-*, justify-*, fit/auto, text-<size>, text-<align>, font-<weight>
0.8.0opacity-*, whitespace-*, leading-*

Check what the emitted runtime actually resolves:

Terminal window
grep -o 'startsWith(token, "[a-z0-9-]*")' out/shared/ui/button.luau | sort -u

Whatever is missing there is missing at runtime, whatever the static path says. The CLI’s vela-rbxts@^0.9.0 floor keeps you above the whole list — facet doctor is what verifies you are actually on it.

Out of local registers

Out of local registers when trying to allocate CardHeader

Pointing at generated code nobody wrote. You are on Vela 0.8.x. That release inlined Vela’s runtime into every transformed file, spending roughly 96 of Luau’s 200 local registers before the file declared anything of its own — and card does not fit in what is left.

Upgrade to vela-rbxts@^0.9.0, which scopes the runtime into one initializer; emitted files drop from ~106 module-scope locals to ~24.

If you hit this on a component you wrote, the same limit is the cause: each exported name costs a register. card exports one cardVariants object rather than six separate recipes for exactly this reason.

One label is in a different font

Larger, and visibly not the typeface everything else uses. That text recipe has no font-* token. Vela leaves FontFace untouched when none appears, and Roblox’s untouched default is LegacyArial.

Add a weight — font-normal counts. This is not optional styling; it is the only thing that says which font. card’s description shipped this way until someone opened Studio.

A button’s label is 8px and near-black

Two causes, both about className arriving at the wrong place.

You are using asChild. TextSlot never renders on that path, so the label recipe is not applied and the child draws its own text at Roblox’s default. Style the child yourself — buttonLabelVariants is exported for it. See Button.

Or a wrapper re-applied className internally. Vela lowers className at the call site, so <TextSlot className={…}> becomes a runtime host whose resolved TextColor3 / TextSize / FontFace arrive as ordinary props. A component that accepts a className prop and re-applies it inside drops those props instead. TextSlot takes no className for this reason.

The dialog throws the first time it opens

[PortalProvider] context is undefined. Wrap components with <PortalProvider.Provider>.

There is no PortalProvider above your app. Dialog.Portal reads the PlayerGui it renders into from a strict context. Everything before this point succeeds — it compiles, it type-checks, it ships — and the failure lands on a player pressing the button.

The .Provider in the message is Lattice’s generic strict-context wording; the component to reach for is PortalProvider itself.

src/client/main.client.tsx
import { PortalProvider } from "@lattice-ui/react-layer";
root.render(
<PortalProvider container={playerGui}>
<App />
</PortalProvider>,
);

One wrapper for the whole app, not one per dialog. facet add dialog offers to write it for you — what it does and when it refuses — and facet doctor reports it missing.

border-b boxes my element instead of underlining it

A one-sided border is not a thing Roblox can draw. border-* lowers to a UIStroke, which outlines the whole instance, so Vela treats every side-specific key — border-b, border-t, border-x and their prefixed forms — as unsupported and drops it, silently. What is left is border-border, which colours a stroke that then draws on all four sides.

Reach for divide-x / divide-y on the parent instead: Vela lowers those to real one-pixel frames interleaved between its children, so a stack gets a rule between each pair and none after the last. Where you need a single rule in a specific place, put a Separator in the flow by hand.

The registry’s accordion is the worked example — it carries divide-y divide-border on its root for exactly this reason. See Accordion.

A component ignores the size I gave it

The recipe names that property, so it wins. This is the call-site className problem in its most confusing form, because some of the class list survives:

<ScrollArea className="h-32 rounded-md border border-border" />

rounded-md and border-border reach the instance — the root recipe names neither a corner nor a stroke. h-32 does not: the recipe says h-full, and a component’s class-derived props are emitted after its spreads. So the box takes its parent’s height while looking like the class worked.

Put the size on a wrapper frame, or edit the copied file.

A className I pass to a component is ignored

Expected — it cannot work. <Button className="bg-destructive" /> renders exactly like <Button />, silently. Vela consumes the class at the call site and lowers it into instance props, and the component’s own class-derived props are emitted after its spreads, so they overwrite what arrived. Both halves of the override lose.

The answer is the one the copy-in model is built on: edit the copied file. Full explanation, and what does still cross the boundary, in Overriding from the call site.

A className inside a component I edited is ignored

Different problem, and this one is a bug you can fix. Something is appended after props.className. Vela resolves last-token-wins, so a class landing after it is an override nothing can undo:

// wrong
cn(buttonVariants({ variant, className: props.className }), disabled && "bg-muted")
// right
buttonVariants({ variant, className: cn(disabled && "bg-muted", props.className) })

cn deliberately does not resolve conflicts — why.

TS2747 on a string child

<Button>Save</Button> // TS2747
<Button Text="Save" /> // this

roblox-ts React’s ReactNode has no string member. Not a Facet choice — the whole story.

TS2440: Import declaration conflicts with local declaration

On ClassValue. Vela inlines a runtime that declares a local by that name. Import ClassName from ~/lib/utils, which re-exports it under a name that does not collide.

facet add writes nothing

Everything requested is already here. Pass --overwrite to replace it.

add never overwrites by default. --overwrite replaces wholesale — commit first, your edits are gone.

facet remove refuses

“differs from the registry” — the file is not what the registry would write there today. That means you edited it, or upstream moved; Facet records nothing at copy time and cannot tell them apart. --force deletes it anyway.

“is still imported by …” — another installed component depends on it. --force does not cover this one, because deleting utils out from under button leaves a project that does not compile. Remove the dependents first.

Registry errors

MessageMeans
Could not reach the registry at …Network, or a wrong registry value. add needs the network; the registry is fetched, not bundled.
… is format version N, which this CLI does not understandUpgrade facet-rbxts.
Unknown component "x"Run facet list.
Component "x" was not found at …A registry serving an index that disagrees with its own files — a fork’s build problem.

No package.json found above …

facet walks up from the current directory to the nearest package.json and treats it as the project root. Run it inside your roblox-ts project, or pass --cwd.

Tokens do not resolve

tokens 4 token(s) installed components name are not defined: card, card-foreground, …

Your vela.config.ts does not spread facetTheme. doctor reads the config as text when it does not — naming a token as a colour key counts — because a Vela config is arbitrary TypeScript and the alternative to reading it is evaluating it.

The fix is in Theming. If a token is reported as not being in @facet-ui/theme either, that is a Facet bug rather than a project one — upgrade the theme package or report it.

Imports do not resolve after copying

If facet.json sets an import specifier on an alias, your tsconfig needs a matching paths entry; doctor fails on this rather than warning, because every copied component imports through it. Either add the entry, or clear "import" in facet.json and let the CLI write relative imports — which need no paths and work anywhere.

Changing that setting does not rewrite files already copied. facet add <name> --overwrite rewrites them with the new resolution.