Facet publishes three packages, and the components are not among them — they are text on a static registry the CLI fetches and copies in. You install the CLI. It installs the other two when they are 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, published under MIT, and currently at 0.4.0.
What your project has to be first
Facet copies files into a project rather than creating one, so facet init needs 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 edits it — if the check fails, add it yourself:
{ "compilerOptions": { "plugins": [{ "transform": "vela-rbxts/transformer" }] }}Without it className is inert and nothing errors — you get a screen of grey Roblox defaults.
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 everyfv()recipe produces. That 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: the inlined runtime spent roughly 96 of Luau’s 200 local registers before the file declared anything of its own.
^0.9.0 is npm’s 0.x caret, so it is also a ceiling on the next Vela minor — deliberate while every
Vela minor has moved class resolution. Raising it is a CLI release.
Registry components separately declare @lattice-ui/react-runtime@^0.8.0, and a component wrapping
a Lattice primitive declares that primitive’s package at the same floor. Every entry carries the
same spec for a given package, because add unions those strings across the install set.
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. - Creates
vela.config.tsif there is none, pre-wired withfacetTheme(). An existing one 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, so copied files reach each other through relative imports, which needs no tsconfig paths. 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, computed values. A pattern-matched edit that mangles one is worse than a printed snippet.
There is exactly one file the CLI does edit, and it took a real parser. 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 — see wiring a provider.
Verify
npx facet-rbxts doctordoctor checks the whole setup. The config, the import aliases, the transformer, the theme, and
which components are installed. It also checks whether the tokens they name resolve, and whether the
packages underneath meet the floors those files need.
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.