Vela ships a language server, vela-rbxts-lsp, and a VS Code extension that drives it. The server
reuses the compiler crate directly, so what the editor tells you about a class name is what the
compiler would do with it. The same parser, the same theme resolution, the same diagnostic codes.
VS Code
Install Vela LSP from the marketplace, id astra-void.vela-rbxts-lsp. It requires VS Code
^1.91.0.
It activates on TypeScript and TypeScript React documents with a file: scheme. There is nothing to
enable per project — open a .tsx file and the server starts.
Settings
The extension contributes three settings and no commands — changing any of them restarts the client for you.
| Prop | Type | Description |
|---|---|---|
| velaRbxts.lsp.serverPath | string | Explicit path to a server binary. A relative path is resolved against the workspace root. Defaults to "", meaning auto-resolve. |
| velaRbxts.lsp.trace.server | "off" | "messages" | "verbose" | LSP protocol trace level written to the extension's output channel. Defaults to "off". |
| velaRbxts.lsp.enabled | boolean | Whether to start the language client at all. Defaults to true. |
| velaRbxts.inlayHints.enabled | boolean | Show what each class lowers to as an inlay hint after the class. Added in 0.13.0. Defaults to false. |
How the server binary is resolved
velaRbxts.lsp.serverPath, if set, is used directly (resolved against the workspace root when
relative). Otherwise the extension launches the server through process.execPath with
require.resolve("@vela-rbxts/lsp"), gated on the matching platform package being present.
The VSIX bundles the same platforms npm publishes: darwin arm64 and x64, linux x64 gnu and musl, linux arm64 gnu, and win32 x64. Linux arm64 musl and Windows on ARM have no binary on either channel — build the server from source and point velaRbxts.lsp.serverPath at the result.
There is no cargo run fallback. Developing the server itself, build it and set serverPath
explicitly.
What the language server provides
The declared capability set is small and deliberate.
Completions. Offered only when the compiler reports the cursor inside a className context, on
the trigger characters -, :, ", ' and space. There is no resolve step, so every item arrives
complete, and a completion rewrites the segment under the cursor and nothing else.
placeholder-transparent is not offered on a textbox, which the compiler turns down. Every other
family taking the transparent keyword still offers it.
Every capability on this page reads the class strings out of the shapes a className is really written in. Chief among them a function, which is how a Vide project writes a dynamic class value. Also a template’s interpolations, as const, satisfies, string concatenation, and an object’s computed keys and spreads.
Hover. Shows what a class token resolves to. A viewport-scaled offset reads as its rem value
first — Sets `UIPadding` to `1rem` (16px at the base viewport). — in hovers and completion
docs alike. A config that
pins rem gets plain pixel
wording, matching the emit it produces.
Diagnostics. Push-based via publish_diagnostics, debounced 200 ms and gated on the document
version. There is no pull-diagnostics provider.
Document colors and color presentations. Color utilities get a swatch in the gutter and can be edited through the editor’s color picker.
Code actions. Quickfixes driven by diagnostics whose source is vela-rbxts, each offering up to
three ranked replacements plus a “remove token” action. There is also the source action
source.sortVelaClasses below.
Document highlight. Highlights other occurrences of the token under the cursor.
Inlay hints, since 0.13.0. The editor can show what each class lowers to, after the class:
<frame className="p-4 rounded-l-lg" />// ↑ UIPadding.PaddingTop/Right/Bottom/Left// ↑ UICorner.TopLeftRadius, BottomLeftRadiusThey are off by default, behind velaRbxts.inlayHints.enabled. The labels come from the
compiler’s own lowering, read back through an editor API.
That is the complete list. The server does not provide go-to-definition, references, rename, formatting, semantic tokens or signature help. Your editor falls back to its TypeScript language service for them.
Sorting class names
The compiler exposes a canonical class order, offered as the source action source.sortVelaClasses.
It rewrites every className in the document at once — layout, sizing, spacing, colours, radius,
typography, with variant-prefixed tokens last:
<frame className="text-lg bg-blue-600 flex p-4 hover:bg-blue-700 rounded-lg w-full" /><frame className="flex w-full p-4 bg-blue-600 rounded-lg text-lg hover:bg-blue-700" />The sort never changes what your classes do. Utilities that can write the same Roblox property
are treated as one group and keep their relative order within it. A px-4 written after p-2 stays
after it. A template keeps the whitespace around each interpolation.
It does not change how they are laid out either. Whitespace between tokens is carried over as
written. A class list broken across several lines stays that way. An arbitrary payload containing a
space, such as w-[calc(100% - 4px)], moves as the single class it is. A value whose bracket never
closes is left alone entirely.
Run it from the editor’s source-action menu, or on save:
{ "editor.codeActionsOnSave": { "source.sortVelaClasses": "explicit" }}Any LSP client supporting source actions can invoke it by that kind. The underlying API is
sortClassNames on @vela-rbxts/compiler. It takes { source, fileName } and returns
{ edits: [{ range: { start, end }, text }] }. Those are the same offsets the server turns into
workspace edits.
Text synchronization is incremental and the position encoding is UTF-16.
The rule that text-* belongs on textlabel/textbutton/textbox, image-* on imagelabel/imagebutton and placeholder-* on textbox is enforced only in the editor. <frame className="text-red-500" /> shows a squiggle in VS Code and compiles without complaint, emitting TextColor3 onto a Frame. Treat it as a lint.
Inside a component’s className the rule is dropped rather than merely unenforced. The editor cannot know which host element <Panel /> renders, so it completes and hovers the full utility set there and never raises unsupported-host-utility.
The editor also suppresses unknown-theme-key while the payload typed so far is still a prefix of a
real key. bg-sla never flashes a warning on the way to bg-slate-800, while bg-nope warns
immediately.
A vela.config.ts the extension fails to load raises a notification naming the file and the reason, and a config that loads on a later save clears it. Without that the session stays silently on the default theme, reporting every key the project defines as unknown.
Other editors
The server speaks LSP over stdio and has no VS Code dependency. Spawn it with:
npx --package @vela-rbxts/lsp vela-rbxts-lspThe binary is named vela-rbxts-lsp but owned by the @vela-rbxts/lsp package. vela-rbxts-lsp on
its own is the VS Code extension id, published as a VSIX, so a bare npx vela-rbxts-lsp resolves
nothing.
The server does not read vela.config.ts from disk. It has no file loader and no config
discovery. The client evaluates each config and hands over the result. Without that, the server
falls back to the built-in default theme and every custom key reports unknown-theme-key.
Pass the configs in initializationOptions:
{ "workspaceRoot": "/abs/path/to/project", "configs": [ { "dir": "/abs/path/to/project", "json": "{\"theme\":{\"colors\":{ }}}" } ]}workspaceRoot and configs are top-level siblings. Get the shape wrong and there is no error. The
server deserializes with from_value(...).ok().unwrap_or_default(), so anything unparseable becomes
zero configs and you are back on the default theme.
Each entry is a directory plus the resolved config serialized as JSON. The server matches a file to the nearest containing dir, which is how per-package configs in a monorepo work.
To update configs after startup, send the custom method vela-rbxts/setConfigs. Its payload carries
only configs. That is what the VS Code extension does. It watches **/vela.config.{ts,json},
evaluates each match through @vela-rbxts/rbxtsc-host/project-config, and pushes { dir, json }
pairs at startup and on every change.
A client that cannot evaluate TypeScript can build the same JSON any way it likes — the server only
cares that the shape matches the resolved TailwindConfig.
Platform packages
The vela-rbxts-lsp launcher resolves one of six platform-specific packages:
@vela-rbxts/lsp-darwin-arm64@vela-rbxts/lsp-darwin-x64@vela-rbxts/lsp-linux-arm64-gnu@vela-rbxts/lsp-linux-x64-gnu@vela-rbxts/lsp-linux-x64-musl@vela-rbxts/lsp-win32-x64-msvc
Those six are what the release workflow publishes and what the VSIX bundles. The launcher’s lookup
table also names @vela-rbxts/lsp-linux-arm64-musl and @vela-rbxts/lsp-win32-arm64-msvc, but
nothing builds them — on those two, build from source.
See also
- Configuration — the config shape the server expects.
- Diagnostics — every code the editor can surface, and which ones also appear in a build.
- Dynamic class names — the case the editor cannot help you with, because nothing is reported at all.