Facet publishes three packages, and the components are not among them. They are text on a static registry that the CLI fetches and copies into your project. You install the CLI; it installs the other two at the moment they are actually needed.
pnpm add -D facet-rbxtsnpm install -D facet-rbxtsyarn add -D facet-rbxtsbun add -d facet-rbxtsOr skip installing it at all — every command works through npx:
npx facet-rbxts initnpx facet-rbxts add buttonThe three packages, and which one arrives when
| Package | What it is | Installed as |
|---|---|---|
facet-rbxts | The CLI. Command is facet. Node, dev dependency. | -D, or npx |
@facet-ui/theme | Semantic tokens shaped as a Vela config preset. Only vela.config.ts imports it. | -D, by facet init |
@facet-ui/react-variants | fv() and cn() — the one runtime package copied components import. | runtime, by facet add |
All three are versioned in lockstep (fixed in the changesets config), published under MIT, and
currently at 0.4.0.
What your project has to be first
Facet copies files into a project; it does not create one. Before facet init is useful, the
project needs to be a roblox-ts project that already builds, with three
things in place:
| Requirement | Why |
|---|---|
@rbxts/react | Components are @rbxts/react function components. Vide is not supported. |
Vela, with its transformer registered in tsconfig.json | Every visual property a component has arrives through className. Without the transformer the build still succeeds and the UI comes out completely unstyled. |
A package.json at the project root | facet walks up from the current directory to the nearest one and treats it as the project root. |
Lattice is not on that list: facet add installs the Lattice packages a component needs, because
the registry entry declares them.
facet init and facet doctor both check tsconfig.json for vela-rbxts/transformer — textually,
because roblox-ts tsconfigs are routinely JSONC. Neither of them edits it. If the check fails, add
it yourself:
{ "compilerOptions": { "plugins": [{ "transform": "vela-rbxts/transformer" }] }}Without it, className is inert. Nothing errors. You get a screen of grey Roblox defaults and no
explanation.
Version floors: why the CLI pins vela-rbxts@^0.9.0
The CLI installs vela-rbxts@^0.9.0 as a build dependency, and that floor is not decorative:
- Below 0.7.0,
w-fitandfont-*silently do nothing on the computed-classNamepath that everyfv()recipe produces — which is how the first publishedbuttonshipped zero pixels wide. - Below 0.8.0,
opacity-*,whitespace-*andleading-*join them. - Below 0.9.0,
carddoes not compile at all. Vela 0.8.0 inlined its runtime into every transformed file, spending roughly 96 of Luau’s 200 local registers before the file declared anything of its own;cardfailed withOut of local registerspointing at generated code nobody wrote. 0.9.0 scopes that runtime into a single initializer.
The caret is npm’s 0.x caret, so ^0.9.0 means >=0.9.0 <0.10.0 — a floor on a fresh init and a
ceiling on the next Vela minor. That is deliberate while Vela is pre-1.0 and every minor so far has
moved class resolution. Raising it is a CLI release, which is the point at which the registry has
actually been built against the new minor.
Registry components separately declare @lattice-ui/react-runtime@^0.8.0. 0.8.0 is what made
asChild work at all — see Components.
A component that wraps a Lattice primitive declares that primitive’s package at the same floor —
@lattice-ui/react-checkbox@^0.8.0, @lattice-ui/react-dialog@^0.8.0, and so on — and facet add
installs it when the component is copied. Every entry carries the same spec for a given package,
because add unions those strings across the install set and two spellings would both reach the
package manager. registry:check enforces it; the
registry format reference has the rule.
facet init
npx facet-rbxts initIt asks four things (or takes the defaults with -y), then does five:
- Writes
facet.json— theme base and mode, where components land, which registry to read. See thefacet.jsonreference. - Creates
vela.config.tsif there is none, pre-wired withfacetTheme(). If one already exists it is never rewritten — only reported on, with the exact lines to add. - Installs the build dependencies —
@facet-ui/themeandvela-rbxts@^0.9.0, as dev dependencies. - Adds the
utilsregistry item, because every component imports~/lib/utils. That also pulls in@facet-ui/react-variants. - Reports on
tsconfig.json— the transformer check above.
The defaults, if you pass -y
{ "$schema": "https://facet.astra-void.xyz/schema.json", "style": "default", "theme": { "base": "zinc", "mode": "dark" }, "aliases": { "ui": { "dir": "src/shared/ui" }, "lib": { "dir": "src/shared/lib" }, "hooks": { "dir": "src/shared/hooks" } }, "velaConfig": "vela.config.ts"}No import specifier is set on those aliases by default, which means copied files reach each other
through relative imports. That needs no tsconfig paths and therefore works in a project nobody
configured for this. If your project already has an alias, answer the prompt with it and the CLI
writes shared/ui-style specifiers instead.
Why init creates files but never edits them
Both vela.config.ts and tsconfig.json belong to you and are routinely non-trivial — JSONC,
comments, spreads, plugins, computed values. A pattern-matched edit that mangles one is worse than a
printed snippet, so init prints.
There is now exactly one file the CLI does edit, and it took the real parser that condition
implied: facet add wraps your client entry in the providers a component declares, using
@babel/parser for positions and string splices for the edit. It is behind a prompt, and it
happens in add rather than init — see wiring a provider.
Verify
npx facet-rbxts doctordoctor is the one command that checks the whole setup rather than one part of it: 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. A clean run
means a copied component will compile and look like it is supposed to.
Then add something and build:
npx facet-rbxts add buttonnpx rbxtscSee Your first component for what to do with it.
Next step
- Your first component — render a
Button. - How it works — what
addactually does to the files. - Scope and status — what exists at 0.4.0, and what does not.