Velagetting started

Installation

Packages, the two ways to run Vela, the config file, and the ambient declaration.

Vela is a compile-time utility-class system for roblox-ts UI — React by default, Vide when the project says so. Write Tailwind-shaped class names on Roblox host elements and your own components, and Vela rewrites them into Roblox properties and helper instances before roblox-ts emits Luau.

It is a compiler over your .tsx files, not a component library. Where that compiler runs is yours to pick — a plugin inside rbxtsc, or a command that runs before it.

Install the packages

pnpm add vela-rbxts

Then the ordinary roblox-ts stack, which Vela does not bring for you. Install the UI library you actually write JSX with — React:

pnpm add @rbxts/react @rbxts/react-roblox @rbxts/services

…or Vide, which needs no Vela config of its own — the target is read off the jsxFactory your tsconfig.json already sets, see the Vide guide:

pnpm add @rbxts/vide @rbxts/services
pnpm add -D roblox-ts typescript@5.9.3 @rbxts/compiler-types @rbxts/types

The typescript@5.9.3 pin is deliberate: Vela calls into your TypeScript install, and 7.x fails on the first file. Nothing enforces it.

What the TypeScript pin protects you from, and the rest of the version matrix

Vela’s host adapter resolves typescript from your install and calls into its API. TypeScript 7 moved enough of that API to break the call. A plain typescript install today gets 7.x and a build that dies on the first file:

rbxtsc output with typescript@7
src/client/Panel.tsx:1:1 - error TS89000: [@vela-rbxts/rbxtsc-host] compiler-invocation-failed: Cannot read properties of undefined (reading 'Latest')

5.9.3 is what the repo pins and tests against, and 6.x also builds. No package declares a peerDependency, so your package manager will not warn you, and the error names neither TypeScript nor a version.

The rest of what Vela is developed against: Node 24 in CI, roblox-ts ^3.0.0, @rbxts/compiler-types ^3.0.0-types.0, @rbxts/types ^1.0.935, @rbxts/react ^17.3.7-ts.1, and @rbxts/services ^1.6.0.

Why no runtime host appears in those commands

An element Vela could not settle at compile time imports a runtime host: @rbxts/vela-runtime for React, @rbxts/vela-runtime-vide for Vide. Both ship as dependencies of vela-rbxts, and each declares its UI library as an optional peer. The host you do not emit for is one inert ModuleScript.

@rbxts/services matters beyond your own imports. The host reads { UserInputService, Workspace } from it for viewport, input mode and rem, so it must resolve in the built place. @rbxts/react-roblox mounts a React tree, and Vide mounts itself.

Choose how Vela runs

Two ways to run it, the same compiler either way. The transformer registers a plugin so the lowering happens inside rbxtsc. The CLI lowers your sources into a second tree first, which rbxtsc then compiles with no plugin.

They differ in where the work sits in your build:

TransformerCLI
SetupOne tsconfig.json plugin entryrootDir/include point at the generated tree
Build commandrbxtscvela build && rbxtsc
Watchrbxtsc -wvela watch beside rbxtsc -w
Sources rbxtsc seesYoursThe generated mirror
Lowered outputNever written to diskA reviewable tree you can read and diff
Needs a plugin-capable toolchainYesNo
Processes to keep runningOneTwo
Available since0.1.00.11.0

Pick the transformer when rbxtsc is your build. Pick the CLI when a plugin is not an option, such as a pinned toolchain or a build system driving tsc itself. It also fits when you want the lowered sources as an artifact.

Everything after this section is shared: the config file, the type declaration, and the smoke test all work the same on both.

Track A — the transformer

Add one entry to compilerOptions.plugins in your existing tsconfig.json:

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

That is the entire tsconfig change — Vela requires no compiler option of its own. Without a project yet, start from roblox-ts’s quick start.

Why the entry names a /transformer subpath, and what else goes in it

vela-rbxts/transformer is a separate export from the package root: the root gives you defineConfig and types, the subpath is the callable roblox-ts consumes.

The plugin entry object is also where transformer options go: file filters, a diagnostic code base, a project root. All are covered in the configuration reference.

Track B — the CLI

Point rbxtsc at the tree the CLI generates, and register no plugin:

tsconfig.json
{
"compilerOptions": {
"rootDir": ".vela/src",
"baseUrl": ".vela/src",
"outDir": "out"
},
"include": [".vela/src"]
}

vela build mirrors src into .vela/src, transforming the .tsx files that use className and copying everything else byte for byte. It writes a .gitignore beside the tree:

Build
npx vela build && npx rbxtsc -p tsconfig.json

If a plugin entry is still registered, or rootDir still points at src, vela build says so on every run. The CLI reference has every flag.

Edit src, never the generated tree

The mirror is a build output: an edit inside .vela/src survives until the next run touches that file, with no warning when it disappears. Your editor and tsc --noEmit point at the generated tree too, but the language server still reads your real sources.

Add vela.config.ts

Optional — with no config file Vela uses its default theme and infers the framework from tsconfig.json. Add it when you want your own theme values:

vela.config.ts
import { defineConfig } from "vela-rbxts";
export default defineConfig({
theme: {
extend: {
colors: {
brand: {
500: "Color3.fromRGB(59, 130, 246)",
700: "Color3.fromRGB(29, 78, 216)",
},
},
radius: {
panel: "new UDim(0, 10)",
},
},
},
});

Two things to get right. Every value is an expression string in the roblox-ts dialect, and it goes under theme.extend. A top-level theme.colors replaces the whole registry and discards theme.extend.colors silently. See the theming guide.

Where the file goes, and where Vela looks for it

Vela walks up from each source file’s directory looking for vela.config.ts or vela.config.json, the .ts form winning when both exist. There are no .js, .mjs or .cjs variants and no package.json key.

Keep it at the project root, outside rootDir — Vela loads it with Node, so it is build tooling rather than game code. Dropped into src/, roblox-ts sweeps it up and fails the build:

rbxtsc output
src/vela.config.ts:1:30 - error TS roblox-ts: You cannot use modules directly under node_modules.
1 import { defineConfig } from "vela-rbxts";
~~~~~~~~~~~~

Note that this is the opposite of the .d.ts file below, which must live inside rootDir.

One wrinkle at the root: a roblox-ts tsconfig.json includes only src, so typed ESLint reports vela.config.ts as outside the project. Widening include hands the file back to roblox-ts — use an ESLint override for that path, or the JSON form.

Declare the className prop

Roblox host elements have no className prop in @rbxts/types. Vela augments React.Attributes globally, which takes effect only if the package is imported for its side effect somewhere in your program:

src/vela-env.d.ts
import "vela-rbxts";

Without it every className is a type error, even though Vela would still have processed it. Keep the file inside src — under the CLI, .d.ts files are copied through unchanged.

Install the language server

Vela ships a language server, vela-rbxts-lsp, built from the same compiler crate the build uses. No build needs it, but one class of mistake is reported only here: text-* on a frame, image-* on a label, placeholder-* outside a textbox. The transform pipeline never checks any of it, so those compile clean and write the wrong property onto the wrong instance.

In VS Code, install Vela LSP, id astra-void.vela-rbxts-lsp, publisher astra-void:

VS Code
code --install-extension astra-void.vela-rbxts-lsp

It needs VS Code ^1.91.0, and that is the whole install — the marketplace serves a per-platform VSIX with the server binary inside it. The extension activates on TypeScript and TSX documents: open a .tsx file and the server starts.

How your theme reaches the server

The extension watches vela.config.{ts,json}, evaluates every match and pushes the result over the wire — the server never reads the file itself. The .ts form goes through your project’s typescript, so the pin at the top of this page decides whether your editor resolves custom theme keys too.

Any other editor

The server speaks LSP over stdio and has no VS Code dependency. Add it to the project:

pnpm add -D @vela-rbxts/lsp

Then point your client’s server command at it:

Server command
npx --package @vela-rbxts/lsp vela-rbxts-lsp

--package is not optional. The binary is named vela-rbxts-lsp, but the package owning it is @vela-rbxts/lsp. vela-rbxts-lsp alone is the VS Code extension id, published as a VSIX. The platform binary arrives as an optional dependency picked by os/cpu. Linux arm64 musl and Windows on ARM have no prebuilt binary on either channel and need a source build.

Nothing hands the server your config here. Passing the resolved theme is the client’s job, through initializationOptions. Skip it and the server falls back to the default theme, so every custom key reports unknown-theme-key against classes that build perfectly well.

Editor setup has the payload shape, the method that updates it after startup, and the full list of what the server does and does not implement.

Build and serve

On track A there is no Vela-specific build step — compile with roblox-ts as usual:

Build — track A
npx rbxtsc

On track B, lower first. vela build exits non-zero when a file fails to compile, so && is the right joint:

Build — track B
npx vela build && npx rbxtsc

Either way, sync out/ into Studio with Rojo the same way you already do:

Serve
rojo serve

You need no Vela-specific Rojo mapping.

Why the runtime import resolves with no project change

Most of what Vela produces is a Roblox property or a helper instance emitted as a JSX child. The rest is an import of @rbxts/vela-runtime, one ModuleScript the whole place shares.

It resolves because roblox-ts only resolves a package whose scope directory is one of the project’s typeRoots. node_modules/@rbxts is the one every roblox-ts project lists and every Rojo template maps to ReplicatedStorage, the same route @rbxts/react takes. This is what the nodeLinker note is about: the directory has to be real for either tool to walk into it.

Check that it actually took

Two of the three ways this setup can fail are silent, so spend one build proving it worked. Put a broken class on any .tsx element:

src/client/Smoke.tsx
<frame className="bg-nope-500 p-4" />

Build and read the output, looking for a warning naming nope-500 — track A routes it through roblox-ts’s numbered diagnostics, track B prints it itself:

Track A — rbxtsc output
src/client/Smoke.tsx:4:28 - warning TS89000: [@vela-rbxts/compiler] unknown-theme-key: Unknown theme key "nope-500" for background color utility in className literal.
4 return <frame className="bg-nope-500 p-4" />;
~~~~~~~~~~~
Track B — vela build output
client/Smoke.tsx:4:28 - warning vela/compiler(unknown-theme-key): Unknown theme key "nope-500" for background color utility in className literal.

If you get the warning, everything is wired: Vela is running and diagnostics are reaching you. Delete the file.

No warning — what to check

className is also a type error in your editor. The ambient declaration is not loading. Check that the .d.ts file exists inside src and is not named src/vela-rbxts.d.ts.

className type-checks fine. On track A, either the transformer is not running, or your roblox-ts does not expose context.addDiagnostic and is discarding every Vela warning. For the first, check that the plugins entry spells the /transformer subpath and the file ends in .tsx. The second makes broken classes look clean forever. On track B, check you ran vela build and that its header names the directory you expected.

What the emitted file should look like

Check the emitted file: out/ on track A, .vela/src on track B. p-4 should have become a <uipadding> child, and the className attribute should be gone. If the attribute survived, the element was skipped.

The padding values read as __VelaRem.scale(new UDim(0, 16), 0) rather than a bare new UDim(0, 16), and the file grows an import of @rbxts/vela-runtime. That is rem: 16 is the value at the base viewport, and the scaler follows the real one.

Next steps

  • Your first component — write a panel, put it on screen, and read exactly what each class lowered to.
  • Editor setup — what the server provides, the three settings it contributes, sorting class names on save, and configuring a non-VS Code client.
  • The playground — compile class strings in the browser against the real compiler, with no project at all.
  • Vide — what changes when the project emits for Vide instead of React, and the two limits that are inherent to it.