Start here:
npx facet-rbxts doctorIt checks the config, the import aliases, the transformer, the theme, which components are
installed, whether their tokens resolve, and whether the packages underneath meet their floors. 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.
{ "compilerOptions": { "plugins": [{ "transform": "vela-rbxts/transformer" }] }}facet init and facet doctor both check for this textually and neither edits the file.
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.For a container, check the whole chain. One that hugs its content can only measure children that already know their own size, so a single 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 Vela resolves at runtime rather than at compile time. That path was a strict
subset of the static lowering for a long time:
| Below Vela | These silently do nothing |
|---|---|
| 0.7.0 | flex-*, items-*, justify-*, fit/auto, text-<size>, text-<align>, font-<weight> |
| 0.8.0 | opacity-*, whitespace-*, leading-* |
Check what the emitted runtime actually resolves:
grep -o 'startsWith(token, "[a-z0-9-]*")' out/shared/ui/button.luau | sort -uWhatever is missing there is missing at runtime. The CLI’s vela-rbxts@^0.9.0 floor clears the
whole list, and facet doctor verifies you are on it.
Out of local registers
Out of local registers when trying to allocate CardHeaderPointing at generated code nobody wrote. You are on Vela 0.8.x, which inlined the runtime into every transformed file, spending roughly 96 of Luau’s 200 local registers before the file’s own first line.
Upgrade to vela-rbxts@^0.9.0, which scopes the runtime into one initializer. Emitted files drop
from ~106 module-scope locals to ~24.
The same limit applies to a component you wrote: each exported name costs a register, which is why
card exports one cardVariants object.
One label is in a different font
Larger, and visibly not the typeface everything else uses. That text recipe has no font-*
token, and Roblox’s untouched default is LegacyArial.
Add a weight — font-normal counts. It is the only thing that says which font.
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.
Or a wrapper re-applied className internally. Vela lowers className at the call site, so
the resolved TextColor3, TextSize and FontFace arrive as ordinary props. A component that
accepts a className and re-applies it inside drops them.
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 succeeds, 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.
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, and facet doctor reports it
missing.
border-b boxes my element instead of underlining it
Roblox cannot draw a one-sided border. border-* lowers to a UIStroke, which outlines the
whole instance, so every side-specific key is
dropped silently.
Reach for divide-x or divide-y on the parent instead. Vela lowers those to real one-pixel frames
interleaved between its children: a rule between each pair, and none after the last. For a single
rule in a specific place, put a Separator in the flow by hand.
The registry’s accordion carries divide-y divide-border on its root for exactly this reason.
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.
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:
// wrongcn(buttonVariants({ variant, className: props.className }), disabled && "bg-muted")
// rightbuttonVariants({ 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" /> // thisroblox-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. 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, and --force does not
cover this one. Remove the dependents first.
Registry errors
| Message | Means |
|---|---|
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 understand | Upgrade 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, because a
Vela config is arbitrary TypeScript. Naming a token as a colour key counts.
The fix is in Theming. A token reported as missing from @facet-ui/theme
too is a Facet bug — 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 rather than warns, because every copied component imports through it. Either
add the entry, or clear "import" and let the CLI write relative imports.
Changing that setting does not rewrite files already copied — facet add <name> --overwrite does.
A component can compile clean, emit exactly the properties you expect, and still look wrong. That is how card’s description shipped in Arial, and the first button zero pixels wide. If nothing above explains what you are seeing, open Studio and look at the instance tree.