All-in-one Roblox UI authoring helper for VS Code. Luix understands the call shapes of every popular Roblox UI framework β React-Luau, Roact, Fusion, and Vide β and provides one consistent layer of editor intelligence on top: prop completion, hover docs, inlay hints, color preview, deprecation diagnostics, workspace-wide component inference, and more.
Whether you write:
-- React-Luau / Roact
e("TextLabel", {
Text = "Hello",
-- type "Back" β suggest BackgroundColor3, BackgroundTransparency, β¦
})
-- Fusion
New "TextLabel" {
Text = "Hello",
-- same suggestions
[OnEvent "MouseEnter"] = onHover,
}
-- Vide
create "TextLabel" {
Text = "Hello",
-- same suggestions, plus event names (Activated, MouseEnter, β¦)
-- appear as regular props
}β Luix offers the same prop completions, the same hover docs, the same color picker, the same inlay hints. One extension, every framework.
A whirlwind tour of what you get out of the box β full details further down.
- Prop completion for every Roblox host class, with type-aware
value snippets (
BackgroundColor3 = Color3.fromRGB(β¦),Size = UDim2.new(β¦), etc.). Works insidee("Frame", { β¦ }),New "Frame" { β¦ },create "Frame" { β¦ }, and Luau backtick template strings. Color3 / UDim / Font props auto-open a suggest dropdown with both built-in constructors and your definedluix.palette/luix.spacing/luix.fontstokens. - Class-name completion the instant you type the opening quote of a factory call β picks the class and sets up the props braces.
- Anchor preset shortcut β type
anchor:tl|t|tr|l|c|r|bl|b|brinside any props table and expand to a pairedAnchorPoint+Position. Plus an auto-detect diagnostic that flagsPosition = UDim2.fromScale(0.5, β¦)without a matching anchor. - RichText support β typing
<insideText = "β¦"opens a tag picker (<b>,<font color="β¦" size="β¦">,<stroke>, β¦), the matching close tag is inserted on accept, attribute completion chains multiple attributes per tag, and the color picker fires oncolor="β¦"values. Warns whenRichText = trueis missing. - Roblox custom-glyph display β Robux / Premium / Verified /
Roblox-Plus PUA characters get inlay-hint labels so you can read
what each
[]-box is. Type:robux:to insert the literal glyph. Add your own (:gbp:βΒ£) vialuix.robloxGlyphs.custom. - Image-asset hover preview β hover any
"rbxassetid://NNNN"to see the actual Roblox CDN thumbnail in the tooltip. - Image-asset gutter previews (opt-in) β once enabled, every asset reference also gets a tiny thumbnail in the gutter. Thumbnails are downloaded once and cached. One-click enable from the Luix sidebar.
- Color picker for every
Color3.fromRGB/new/fromHex/fromHSVliteral, plus a convert between forms code action. UDim2form conversion β swap betweennew/fromOffset/fromScalewhen the value is expressible.- Wrap-in code actions β wrap any element in a
Frame,ScrollingFrame, orFrame + UIListLayout. Framework-aware. - Extract-to-component refactor β right-click an element tree β pulls it out into a new file with only the imports it actually uses, transitively resolved.
N referencesCodeLens above every component definition. Hover-style component docs on the call site (e(MyButton, β¦)β inferred props + extends chain).- Prop validation diagnostics β unknown prop on a host class
(with did-you-mean), duplicate key, wrong enum type, prop hardcoded
in a custom component, missing
AnchorPoint, missingRichText, numeric-range warnings (Transparency = 1.5),TextScaledgotcha (collapses to zero without a fixed-offsetSize), deprecatedFont = Enum.Font.X, typo-dTextColor. Optional WCAG-AA color-contrast warnings (luix.contrastWarnings.enabled). - Design tokens β
luix.palette(colors),luix.spacing(UDim),luix.fonts(Font). TypeColor3./UDim./Font.to surface your named tokens. - Roblox font catalogue β typing inside
Font.fromName("β¦")surfaces 36 built-in Roblox families with their supported weights; theEnum.FontWeight.dropdown then filters to only the weights that family actually ships. Custom families vialuix.customFonts. - Color3 β palette extractor β cursor on any Color3 literal β
Save to
luix.palettecode action. - Frame-stats CodeLens (off by default) β
βΈ N descendants, D layers deepover heavy subtrees so you spot layout bloat. - Project-wide diagnostic summary (off by default) β sidebar
shows
N warnings Β· M errors across X files. - Workspace-wide component inference β
e(MyButton, β¦)gets prop completions inferred from the component's annotations, typed parameter, root element, or centralluix.propsconfig β even across files. - Sidebar β Wally / Rojo / scaffold actions, component browser (tree or flat), and image-cache controls.
| Framework | Call shape | Children | Events |
|---|---|---|---|
| React-Luau | e("Frame", { β¦ }, { children }) |
3rd argument | [React.Event.X] = fn |
| Roact | Roact.createElement("Frame", { β¦ }) |
3rd argument | [Roact.Event.X] = fn |
| Fusion | New "Frame" { β¦ } |
[Children] = { β¦ } |
[OnEvent "X"] = fn |
| Vide | create "Frame" { β¦ } |
inline in same table | plain props (X = fn) |
Toggle which frameworks Luix recognizes via luix.frameworks (default:
all four). Override the factory aliases per-framework via
luix.<framework>.aliases β useful if your codebase aliases the factory
locally, e.g. local r = React.createElement or local n = Fusion.New.
The first argument can be a string ("TextLabel") or an identifier
(MyButton, Components.Button) β Luix handles both.
Type any prop name inside an element table and accept the completion to get a snippet wired up with tab stops:
| What you type | Inserted snippet |
|---|---|
BackgroundColor3 |
BackgroundColor3 = Color3.fromRGB(255, 255, 255), |
Size |
Size = UDim2.new(0, 0, 0, 0), |
Interactable |
Interactable = true|false, (a toggleable choice) |
FontFace |
FontFace = Font.fromName("Montserrat", Enum.FontWeight.Regular), |
Text |
Text = "", (cursor inside the quotes) |
HorizontalAlignment |
HorizontalAlignment = Enum.HorizontalAlignment., |
Works identically across all four frameworks. Toggle with
luix.typeAwareValues.
Color3 placeholders honour luix.color3.defaultFormat β pick
fromRGB (default), fromHex, new, or fromHSV so the inserted
template matches your house style.
Two smart-completion behaviors worth knowing about:
- Trailing-comma awareness. If the line already has a
,after the partial prop name (e.g. you typedBac,then went back to fill it in), the snippet's own comma replaces the existing one rather than doubling it. The cursor still lands cleanly after the comma. - Key-position gating. Prop completions and the
anchor:preset shortcut only surface in key position (start of a new entry). Typing inside a value expression likeFontFace = Font.|doesn't pollute the dropdown withBackgroundColor3/Size/ etc. β those only show up when you're typing a fresh prop name.
For Color3 / UDim / Font props specifically, accepting the prop inserts just the namespace prefix and auto-opens the suggest dropdown so you can pick a constructor or one of your defined tokens β covered in the Design tokens section below.
Type anchor: inside any props table and pick one of nine presets:
| Slug | Anchor + Position |
|---|---|
anchor:tl |
top-left (0, 0) |
anchor:t |
top (0.5, 0) |
anchor:tr |
top-right (1, 0) |
anchor:l |
left (0, 0.5) |
anchor:c |
center (0.5, 0.5) |
anchor:r |
right (1, 0.5) |
anchor:bl |
bottom-left (0, 1) |
anchor:b |
bottom (0.5, 1) |
anchor:br |
bottom-right (1, 1) |
Accepting anchor:br expands to:
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.fromScale(1, 1),Kills the constant AnchorPoint mental math. Pairs with the
AnchorPoint auto-detect diagnostic below β if you write
Position = UDim2.fromScale(0.5, 0.5) first and forget the
AnchorPoint, Luix flags it with a one-click fix.
Cursor anywhere in an element call β π‘ lightbulb offers:
- Wrap in Frame β transparent passthrough container.
- Wrap in ScrollingFrame β vertical scroll with
AutomaticCanvasSize. - Wrap in Frame + UIListLayout β vertical stack container with sane defaults.
Framework-aware. Emits parens form (e(...)) for React/Roact, curried
form (New "..." { [Children] = { ... } }) for Fusion, inline children
for Vide.
Right-click an element call β Luix: Extract to componentβ¦
-- Before
local function HomeScreen()
return e("Frame", { Size = ... }, {
e("Frame", { -- cursor here
Size = ...,
BackgroundColor3 = ...,
}, {
e("UICorner", { CornerRadius = UDim.new(0, 8) }),
e("TextLabel", { Text = "Welcome" }),
})
})
end-- After (HomeScreen.luau)
local Card = require(script.Parent.Card)
local function HomeScreen()
return e("Frame", { Size = ... }, {
e(Card, {})
})
end-- After (Card.luau, freshly written)
local React = require(Packages.react)
local e = React.createElement
local function Card(props)
return e("Frame", {
Size = ...,
BackgroundColor3 = ...,
}, {
e("UICorner", { CornerRadius = UDim.new(0, 8) }),
e("TextLabel", { Text = "Welcome" }),
})
end
return CardImports are pulled across transitively β local e = React.createElement brings React along too, so the new file
compiles immediately. Anything the extracted code doesn't use stays
behind. The new file is written in the same folder as the source; the
component is invoked as e(Card, {}) (React/Roact) or Card {}
(Fusion/Vide β which compose components by direct call rather than via
New/create).
The moment you type e(", Roact.createElement(", New ", create ",
or the Luau backtick form e(`, Luix opens a class picker:
e("Fr|") --> accept "Frame" β e("Frame", { <cursor> })
New "Fr|" --> accept "Frame" β New "Frame" { <cursor> }
create "Fr|" --> accept "Frame" β create "Frame" { <cursor> }When the call has no props table yet, accepting also inserts , { β¦ }
(parens form) or { β¦ } (curried form) with the cursor parked inside
ready for prop completion. When a props table already exists, accepting
swaps just the class name. Synthetic intermediate classes (GuiObject,
UILayout, β¦) are hidden β only types you can actually instantiate
show up.
The VS Code Outline panel and breadcrumbs bar reflect the React tree
of the current file, not just its Lua function structure. Components
named via the Name prop are labeled with that name. Cmd+Shift+O
jumps straight to any element by name.
Every multi-line element gets a small label at its closing punctuation so you can tell what just closed even ten levels deep:
e("Frame", {
Name = "Container",
}, {
e("Frame", {
Name = "Inner",
}, {
e("TextLabel", { Text = "Hi" }) -- βΈ TextLabel
}) -- βΈ Frame (Inner)
}) -- βΈ Frame (Container)New "Frame" {
Name = "Container",
[Children] = {
New "TextLabel" { Text = "Hi" } -- βΈ TextLabel
},
} -- βΈ Frame (Container)Default scope is "ancestors" β hints surface only on the chain
containing the cursor, so the file stays uncluttered. Switch to "all"
via luix.inlayHints.scope.
Color3.fromRGB(R, G, B), Color3.new(R, G, B), Color3.fromHex("#β¦"),
and Color3.fromHSV(h, s, v) all get a swatch in the gutter; click it
for VS Code's color picker. The picker surfaces all four constructor
forms β your existing notation is always offered first so editing
visually never silently flips your codebase from hex to RGB (or vice
versa).
Toggle the Color3 picker via luix.colorPreview.enabled β handy if
another Roblox-API extension provides its own picker and you'd rather
not see two. The RichText color picker (see below) is on a separate
luix.richText.colorPicker toggle so you can keep one without the
other.
Put the cursor on any Color3.fromRGB(β¦), Color3.fromHex(β¦),
Color3.new(β¦), or Color3.fromHSV(β¦) literal and the lightbulb
offers:
π‘ Convert to `Color3.fromRGB(...)`
π‘ Convert to `Color3.fromHex(...)`
π‘ Convert to `Color3.new(...)`
π‘ Convert to `Color3.fromHSV(...)`
Picks any of the four and the actual color is preserved.
Cursor on any UDim2.new(...), UDim2.fromOffset(...), or
UDim2.fromScale(...) literal β lightbulb offers conversion to the
other two forms β but only when the value is actually expressible. For
example, UDim2.new(0.5, 10, 0.5, 5) won't offer fromOffset or
fromScale (it mixes both), but UDim2.new(0, 100, 0, 50) will offer
Convert to UDim2.fromOffset(100, 50).
Hover any string of the form "rbxassetid://NNNN" to see the actual
asset image fetched from Roblox's CDN:
Image = "rbxassetid://1234567", -- hover β 150Γ150 preview of the assetCatches "did I paste the right ID?" bugs without bouncing into the
Roblox website. Works on any string literal, not just Image props.
The thumbnail URL is resolved via Roblox's public
thumbnails.roblox.com API and cached per session.
In addition to the hover, every "rbxassetid://NNNN" reference can
get a tiny thumbnail in the gutter next to its line β same pattern as
vscode-gutter-preview for local .png files. Each thumbnail is
downloaded once and persisted to disk; reopens are instant.
Off by default because it persists files to disk and changes every editor's visual layout. The Luix sidebar shows a one-click Enable image gutter previews entry while the feature is off; click it to flip the setting and see a one-time disclosure of where the cache lives.
Settings:
luix.imageGutter.enabled(defaultfalse) β toggles the feature. The hover preview keeps working either way.luix.imageGutter.cacheLocation(default"global") β"global": cache lives under VS Code's extension storage, shared across every workspace."workspace": cache lives at.luix/assetThumbs/inside the current workspace, with a.luix/.gitignoreauto-written so it doesn't leak into commits.
Sidebar: once enabled and there's anything cached, the Workspace
view shows two entries β "Purge image preview cache" (with a live
N assets β X.X MB size readout) and "Open image cache folder"
(reveals the cache directory in your OS file manager). Both also
available via Cmd+Shift+P β "Luix:". Purging wipes both the global
and workspace locations so flipping cacheLocation mid-project never
strands stale files.
Hover any prop name inside an element table to see its type, the class it was introduced on (walking the Roblox hierarchy), and a deep link to the Roblox reference docs.
Hover a custom-component name (e(MyButton, β¦) β hover MyButton)
to see what Luix has inferred about it: its declared props
(@prop/typed param/auto-detected), the base class it extends, and a
list of forwardable props. Hovering a prop key inside e(MyButton, β¦)
shows whether the prop is component-defined or inherited from the base
class.
Typing < inside a string literal opens a tag picker for every Roblox
RichText tag (<b>, <i>, <u>, <s>, <sc>, <smallcaps>,
<uppercase>, <sub>, <sup>, <comment>, <br/>, <font β¦>,
<stroke β¦>, <mark β¦>):
Text = "Hello <|"
^-- type `<` to surface the tag listAccepting includes the matching close tag with the cursor inside:
Text = "Hello <font color=\"#FF0000\"><cursor></font>"Inside an open <font β¦>, <stroke β¦>, or <mark β¦>, an
attribute-name completion (color, size, face, family, weight,
transparency, thickness, joins) fires so you can chain multiple
attributes the way Roblox supports them:
Text = "<font color=\"#FF0000\" size=\"24\" weight=\"Bold\">Hi</font>"Typing the > that closes an opening tag manually auto-inserts the
matching </font>. Inner attribute quotes adapt to whichever outer Lua
string delimiter you use ("β¦", 'β¦', or Luau's `β¦` template
strings) so attribute values never need backslash escaping.
color="β¦" values inside <font>, <stroke>, and <mark> get an
inline color picker that recognizes both #RRGGBB and rgb(R, G, B)
forms β the round-trip preserves whichever you wrote.
If Text = "<fontβ¦>β¦" references a RichText tag but the same props
table doesn't also set RichText = true, Luix flags it with a warning
and a Set RichText = true quick-fix that inserts the line with
matching indentation. Only fires on string-literal Text values, so
Text = someVar stays silent.
Default snippet color format toggles via
luix.richText.defaultColorFormat (hex / rgb, default hex).
Disable the whole feature via luix.richText.enabled.
Roblox's icon set (Robux U+E002, Premium U+E001, Verified U+E000,
Roblox Plus U+E003) lives in the Unicode private-use area β VS Code's
default fonts render them as [] boxes. Luix adds:
- Inlay-hint labels next to each occurrence so you can tell which box is which while reading code.
- Hover tooltips with the codepoint and Luau
\u{β¦}escape. - A completion: type
:robux:,:premium:,:verified:, or:roblox-plus:inside a string and accept to insert the literal glyph.
Add your own keyboard-unreachable shortcuts via
luix.robloxGlyphs.custom:
Typing :gbp: then expands to Β£. Built-in slugs can't be shadowed.
- React/Roact β typing
[React.Event.(or[Roact.Event.) inside a props table lists the events available on the enclosing class (Activated,MouseEnter,MouseButton1Click, β¦). Same for[React.Change.X]listening to property changes. - Fusion β typing
[OnEvent "Msuggests events as plain strings. (Curried call detection is in place; richer in-bracket completion ships alongside it.) - Vide β events are plain table keys; Luix already merges the class's events into the prop suggestion list for you.
Use a component the way you use a host class:
local GamepassCard = require(script.Parent.GamepassCard)
-- Luix indexes every .lua/.luau file in the workspace at activation.
-- Typing inside e(GamepassCard, { β¦ }) offers the props it can detect.
e(GamepassCard, {
-- suggestions come from GamepassCard.lua's signature, annotations,
-- or its root element. Works whether GamepassCard is React, Fusion,
-- or Vide.
})Four inference signals are checked, listed from least to most explicit:
- Auto-detection from the component's root element. If the
function returns
e("Frame", ...),New "Frame" { β¦ }, orcreate "Frame" { β¦ }, Luix uses that class's props. ---@extends ClassNameand---@prop NAME [type]annotations placed above the function. Lua-LSβstyle triple-dash comments β read by Luix, ignored as a regular comment by every other tool.- Typed
propsparameter β inline literal type (props: { gamepassId: number }) or a same-filetypealias. luix.propscentral config β for components that live outside the workspace or need a global override.
Caveat: suggesting β forwarding. A suggested prop only takes effect if your component actually forwards it. If
GamepassCardhardcodes all itsFrameprops, writingBackgroundColor3 = β¦at the call site does nothing. You'd mergepropsinto the inner table (viatable.cloneor a dictionary-join helper) to make it pass through.
Yellow squigglies, one-click fixes:
Deprecation β toggle with luix.deprecationDiagnostics (default
true):
Font = Enum.Font.GothamBoldβ quick-fix replaces withFontFace = Font.fromName("Gotham", Enum.FontWeight.Bold).TextColor = β¦(missing the trailing3) β quick-fix renames toTextColor3.
Prop validation β toggle with luix.propValidation.enabled
(default true):
- Unknown property on a known Roblox class β
e("Frame", { ScrollingDirection = β¦ })warns "Unknown propertyScrollingDirectiononFrame. Did you meanPosition?" with a Rename toPositionquick-fix (Levenshtein-based suggestion). - Duplicate key in the same props table β
Size = β¦, Size = β¦flags the second assignment as silently overwriting the first. - Wrong enum type β
BorderMode = Enum.Font.Xwarns becauseBorderModeexpectsEnum.BorderMode. - Overridden by component β passing a prop to a custom component
whose root element hardcodes the same prop (and doesn't forward
props.X) surfaces an Information-level hint that the call-site value won't take effect. - Missing AnchorPoint β
Positionset toUDim2.fromScale(0.5, β¦)/(1, β¦)/ etc. with noAnchorPointflagged with an Info-level "addAnchorPoint = Vector2.new(0.5, 0.5)" quick-fix. Stops the classic "why isn't my element centered?" bug at the source. - Numeric-range warnings β
Transparency = 1.5,Rotation = 720,BorderSizePixel = 100, etc. Per-prop bounds. TextScaledgotcha βTextScaled = truewith a pure-scaleSize(or noSize) collapses text to zero; flagged with a clear explanation.
Optional WCAG color-contrast warnings (luix.contrastWarnings.enabled):
- Walks the element tree and flags any
TextColor3whose contrast ratio against the nearest ancestor'sBackgroundColor3is below 4.5:1 (WCAG-AA for normal text). Off by default because it's strict and can pile up on existing codebases. Both colors must be literal Color3 expressions β reactive Fusion/Vide values are skipped to avoid false positives.
RichText (gated by luix.richText.enabled):
Text = "<fontβ¦>"withoutRichText = truein the same props table warns that the tags will render as literal text, with a SetRichText = truequick-fix.
When enabled, e(GamepassCard, { β¦ }) for a component the workspace
knows about but the current file doesn't require gets an Information
diagnostic plus a quick-fix that inserts the require line near your
existing imports.
{
"luix.autoImport.enabled": true,
"luix.autoImport.style": "alias",
"luix.autoImport.aliases": [
{
"filesystemPath": "src/Client/UI/Components",
"robloxPath": "script.Components"
},
{
"filesystemPath": "src/Shared/Packages",
"robloxPath": "ReplicatedStorage.Packages"
}
]
}"style": "relative" produces script.Parentβ¦X chains based on
filesystem position; "style": "alias" substitutes the prefixes above.
Every component definition gets an inline βΈ N references CodeLens
above it. Click to peek every workspace call site (e(MyButton, β¦) and
friends) β handy for figuring out blast radius before changing a
component's props.
Toggle with luix.componentReferencesLens.enabled (default true).
When enabled, every element call gets a βΈ Frame β N descendants, D layers deep CodeLens. Useful for spotting subtrees that have grown
out of hand (Roblox slows down once you nest too many UI instances).
luix.frameStatsLens.enabled(defaultfalse).luix.frameStatsLens.minDescendants(default5) β only show the lens for elements with at least this many descendants. Stops trivial elements from cluttering the gutter.
When enabled, the Luix sidebar shows a line summarising every diagnostic VS Code currently knows about for Lua/Luau files in the workspace:
β Project diagnostics β 0 warnings across 12 files (clean)
β Project diagnostics β 8 warnings across 4 files (issues)
β Project diagnostics β 2 errors Β· 5 warnings ... (errors)
Click to open VS Code's Problems panel. Aggregates Luix's own diagnostics plus anything any other extension publishes β useful as a "how clean is my project?" gauge during cleanup passes.
Toggle with luix.workspaceValidation.enabled (default false).
Luix adds an Activity Bar entry with two views:
Workspace β context-sensitive project actions:
| Entry | Visible when | What it does |
|---|---|---|
| βΈ Regenerate Wally types | wally.toml exists |
Runs wally install β rojo sourcemap β wally-package-types in one chained command. |
| βΈ wally install | wally.toml exists |
Just wally install. |
| βΈ Generate Rojo sourcemap | *.project.json exists |
rojo sourcemap <project> -o sourcemap.json |
| βΈ New React component | always | Prompts for a name, creates <Name>.luau with a React-Luau scaffold, opens it. |
| βΈ New Fusion component | always | Same, Fusion New "Frame" { [Children] = { β¦ } } template. |
| βΈ New Vide component | always | Same, Vide create "Frame" { β¦ } template. |
All Wally/Rojo commands stream to a reusable named terminal ("Luix") so you can watch and interrupt them.
Components β every UI component the workspace indexes. Two view modes, toggled via the title-bar button:
- Tree (default): grouped by folder, mirroring how the files are organized on disk. Click an entry to jump to the function definition.
- Flat: alphabetical list of every component.
Only functions Luix is confident are UI components show up here β i.e.
those that either return an e("β¦", β¦) / New "β¦" { β¦ } /
create "β¦" { β¦ } element at the top level, or carry an explicit
---@extends ClassName annotation. Helper functions that happen to
take a props parameter are skipped.
Optionally pin the tree to a subfolder via luix.componentsRoot:
{
"luix.componentsRoot": "src/Client/UI/Components"
}When set, tree mode is rooted there; anything outside is hidden in tree mode (flat mode still shows everything).
To create a new component in a specific folder, right-click that folder in VS Code's Explorer and pick Luix: New component hereβ¦ β Luix prompts for the framework (React / Fusion / Vide) and the name, then writes the file directly into that folder. The sidebar's "New β¦ component" buttons still work too; they open a folder picker first.
Both views are also available via Cmd+Shift+P β search "Luix:".
Three central tables let you name design tokens once and surface them as completions wherever they're relevant:
| Setting | Triggers after | Suggests entries like |
|---|---|---|
luix.palette |
Color3. |
palette.primary β Color3.fromRGB(124, 92, 255) |
luix.spacing |
UDim. |
spacing.md β UDim.new(0, 16) |
luix.fonts |
Font. |
fonts.display β Font.fromName("Gotham", Enum.FontWeight.Bold) |
{
"luix.palette": {
"primary": "Color3.fromRGB(124, 92, 255)",
"surface": "Color3.fromRGB(28, 30, 38)"
},
"luix.spacing": {
"xs": "UDim.new(0, 4)",
"sm": "UDim.new(0, 8)",
"md": "UDim.new(0, 16)",
"lg": "UDim.new(0, 24)"
},
"luix.fonts": {
"display": "Font.fromName(\"Gotham\", Enum.FontWeight.Bold)",
"body": "Font.fromName(\"SourceSansPro\", Enum.FontWeight.Regular)"
}
}The accepted suggestion replaces the trigger keyword with the literal
expression, so the on-disk code stays canonical Luau β no runtime
palette.primary references to resolve.
Save Color3 to palette β cursor on any Color3.fromRGB(...) /
fromHex(...) / new(...) / fromHSV(...) β π‘ Save Color3 to
luix.paletteβ¦. Prompts for a name and a target (User / Workspace
settings); the literal becomes a permanent palette entry. Doesn't
modify the existing call site β it just makes the color reusable
going forward.
{
"luix.palette": {
"primary": "Color3.fromRGB(124, 92, 255)",
"background": "Color3.fromRGB(21, 21, 26)",
"surface": "Color3.fromRGB(28, 30, 38)",
"text": "Color3.fromRGB(255, 255, 255)"
}
}In a Lua file:
BackgroundColor3 = Color3.| -- typing `.` shows:
-- ββ built-in constructors ββ
-- fromRGB Color3 from 0-255 RGB channels
-- fromHex Color3 from a "#RRGGBB" hex string
-- new Color3 from 0-1 RGB channels
-- fromHSV Color3 from 0-1 H/S/V
-- ββ palette tokens ββ
-- palette.primary
-- palette.surface
-- β¦
-- Picking a constructor (e.g. `fromRGB`) inserts the full call with
-- per-channel tab stops so you can quickly type 124, 92, 255.
-- Picking `palette.surface` replaces `Color3.` with the full
-- `Color3.fromRGB(28, 30, 38)` expression.Same pattern applies to UDim. (constructors + luix.spacing
tokens) and Font. (constructors + luix.fonts tokens).
Inside Font.fromName("β¦"), the family dropdown surfaces 36 built-in
Roblox families with their supported weights tagged in the detail line.
The most-used UI families (BuilderSans, Gotham, Roboto, SourceSansPro,
β¦) sort first:
FontFace = Font.fromName("|", Enum.FontWeight.Regular)
^-- dropdown:
BuilderSans Roblox font Β· 7 weights
Gotham Roblox font Β· 6 weights
Roboto Roblox font Β· 9 weights
SourceSansPro Roblox font Β· 6 weights
β¦The Enum.FontWeight. dropdown then filters to only the weights
the active family actually ships:
FontFace = Font.fromName("Cartoon", Enum.FontWeight.|)
^-- Regular (decorative font, only ships Regular)
FontFace = Font.fromName("Roboto", Enum.FontWeight.|)
^-- Thin, ExtraLight, Light, Regular,
Medium, SemiBold, Bold, ExtraBold, Heavy
(all nine)Custom families. Roblox supports custom font assets β register
yours via luix.customFonts and they'll surface in the same
completions, tagged Custom font, sorted above built-ins:
{
"luix.customFonts": {
"MyBrandSans": ["Light", "Regular", "Medium", "Bold"],
"MyBrandSerif": ["Regular", "Bold"]
}
}Weight names must be valid Enum.FontWeight members (the JSON schema
enforces this in VS Code's settings UI). If a custom family shares a
name with a built-in, the custom weight list wins.
Type the prefix, press Tab:
| Prefix | Frameworks | What it inserts |
|---|---|---|
eFrame / eTextLabel / eTextButton / eImageLabel / eImageButton / eScrollingFrame |
React-Luau | e("X", { β¦ }, { β¦ }) |
nFrame / nTextLabel / nTextButton |
Fusion | New "X" { β¦ } with [Children] slot |
cFrame / cTextLabel / cTextButton |
Vide | create "X" { β¦ } with inline children |
eUIListLayout / eUIGridLayout / eUIPadding / eUICorner / eUIStroke |
any | the corresponding utility |
useState / useEffect / useMemo / useCallback / useRef |
React-Luau hooks | the hook call |
reactEvent |
React-Luau | [React.Event.X] = function(rbx) β¦ end, |
rfc |
React-Luau | function-component scaffold |
Two forms work, and they compose:
---@extends Frame
---@prop gamepassId number
---@prop layoutOrder number?
local function GamepassCard(props): React.ReactNode
return e("Frame", { β¦ })
endtype GamepassCardProps = {
gamepassId: number,
layoutOrder: number?,
}
local function GamepassCard(props: GamepassCardProps)
return New "Frame" { β¦ }
endThe ---@extends directive declares the class the component conceptually
extends β its prop list gets merged into the component's suggestions.
---@prop adds explicit per-component props on top. The typed parameter
form does the same thing via Luau types.
All settings live under the luix.* prefix. Open Cmd+, and search
"Luix" to see them in the UI, or write them directly into your
settings.json.
{
// Toggle which frameworks Luix scans for.
"luix.frameworks": ["react", "roact", "fusion", "vide"],
// Override per-framework factory aliases (leave empty to use defaults).
"luix.react.aliases": [], // defaults: ["e", "createElement", "React.createElement"]
"luix.roact.aliases": [], // defaults: ["Roact.createElement"]
"luix.fusion.aliases": [], // defaults: ["New", "Fusion.New"]
"luix.vide.aliases": [] // defaults: ["create", "vide.create"]
}{
"luix.props": {
// Array form β override the prop list for a class.
"Frame": ["Size", "Position", "BackgroundColor3"],
// Empty array disables suggestions for that class.
"TextBox": [],
// Custom component, flat list.
"MyButton": ["label", "onClick", "disabled"],
// Custom component that extends a Roblox class plus extras.
"GamepassCard": {
"extends": "Frame",
"props": ["gamepassId", "layoutOrder"]
}
}
}{
"luix.documentSymbols.enabled": true,
"luix.colorPreview.enabled": true,
"luix.inlayHints.enabled": true,
"luix.inlayHints.scope": "ancestors", // or "all"
"luix.inlayHints.position": "after-comma", // or "before-comma"
"luix.deprecationDiagnostics": true,
"luix.warnReservedPropNames": false,
"luix.typeAwareValues": true,
"luix.snippetMode": "value-with-comma" // or "value" / "name-only"
}{
"luix.autoImport.enabled": false,
"luix.autoImport.style": "relative", // or "alias"
"luix.autoImport.aliases": [
{ "filesystemPath": "src/Client/UI/Components", "robloxPath": "script.Components" }
]
}luix.indexPersistence.enabled(defaulttrue) β persists the parsed component index across sessions; unchanged files skip re-parsing on cold start. No behavioral difference; speeds up activation on large workspaces. Disable to keep Luix offline.luix.useRobloxApiDump(defaultfalse) β fetch the community-maintained Mini-API-Dump once a day and add any new properties Roblox has shipped to the existing completion lists. Additive only β the hand-curated built-in data still wins on conflicts so a stale or partial fetch never breaks existing completions.
- Parsing is text-based, not AST-based. Strings, comments, and Luau
block structure are tracked, but pathological inputs (very unusual
macro/codegen output, type intersections like
Frame & Foo, generics likeProps<T>) can confuse the detector. - Cross-file lookups are name-based. If two files declare a
component called
Button, the first one scanned wins. Pin the intended one vialuix.propsif it matters. - First top-level return wins. Components that conditionally return different element classes have the first one used as the implicit base.
- Suggesting β forwarding. Luix shows what a component could accept; making it actually accept those props is on the implementation.
npm install
npm run compile # one-shot
npm run watch # rebuild on save
npm run lint
npm test # headless VS Code with the extension loaded
npm run build-icon # rerender assets/icon.png from assets/icon.svgPress F5 from this folder in VS Code to launch an Extension Development Host with Luix loaded.
{ "luix.robloxGlyphs.custom": { "gbp": "Β£", "euro": "β¬", "yen": "Β₯", "shrug": "Β―\\_(γ)_/Β―" } }