Skip to content

ericplane/Luix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Luix

Install β€” VS Code Marketplace GitHub stars License

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.


Highlights

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 inside e("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 defined luix.palette / luix.spacing / luix.fonts tokens.
  • 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|br inside any props table and expand to a paired AnchorPoint + Position. Plus an auto-detect diagnostic that flags Position = UDim2.fromScale(0.5, …) without a matching anchor.
  • RichText support β€” typing < inside Text = "…" 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 on color="…" values. Warns when RichText = true is 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: β†’ Β£) via luix.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/fromHSV literal, plus a convert between forms code action.
  • UDim2 form conversion β€” swap between new / fromOffset / fromScale when the value is expressible.
  • Wrap-in code actions β€” wrap any element in a Frame, ScrollingFrame, or Frame + 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 references CodeLens 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, missing RichText, numeric-range warnings (Transparency = 1.5), TextScaled gotcha (collapses to zero without a fixed-offset Size), deprecated Font = Enum.Font.X, typo-d TextColor. Optional WCAG-AA color-contrast warnings (luix.contrastWarnings.enabled).
  • Design tokens β€” luix.palette (colors), luix.spacing (UDim), luix.fonts (Font). Type Color3. / UDim. / Font. to surface your named tokens.
  • Roblox font catalogue β€” typing inside Font.fromName("…") surfaces 36 built-in Roblox families with their supported weights; the Enum.FontWeight. dropdown then filters to only the weights that family actually ships. Custom families via luix.customFonts.
  • Color3 β†’ palette extractor β€” cursor on any Color3 literal β†’ Save to luix.palette code action.
  • Frame-stats CodeLens (off by default) β€” β–Έ N descendants, D layers deep over 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 central luix.props config β€” even across files.
  • Sidebar β€” Wally / Rojo / scaffold actions, component browser (tree or flat), and image-cache controls.

Supported frameworks

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.


Feature tour

Prop completion with type-aware value snippets

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 typed Bac, 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 like FontFace = Font.| doesn't pollute the dropdown with BackgroundColor3 / 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.

Anchor preset completion

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.

Wrap-in code actions

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.

Extract-to-component refactor

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 Card

Imports 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).

Class-name completion inside factory calls

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.

Outline + breadcrumbs

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.

Inlay hints at closing brackets

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.

Color preview

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.

Convert Color3 between formats

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.

Convert UDim2 between forms

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).

Image-asset thumbnail in hover

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 asset

Catches "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.

Image-asset gutter previews (opt-in)

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 (default false) β€” 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/.gitignore auto-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 documentation

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.

RichText support

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 list

Accepting 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 custom-glyph support

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:

{
  "luix.robloxGlyphs.custom": {
    "gbp":   "Β£",
    "euro":  "€",
    "yen":   "Β₯",
    "shrug": "Β―\\_(ツ)_/Β―"
  }
}

Typing :gbp: then expands to Β£. Built-in slugs can't be shadowed.

Event completion

  • 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 "M suggests 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.

Workspace-wide component inference

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:

  1. Auto-detection from the component's root element. If the function returns e("Frame", ...), New "Frame" { … }, or create "Frame" { … }, Luix uses that class's props.
  2. ---@extends ClassName and ---@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.
  3. Typed props parameter β€” inline literal type (props: { gamepassId: number }) or a same-file type alias.
  4. luix.props central 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 GamepassCard hardcodes all its Frame props, writing BackgroundColor3 = … at the call site does nothing. You'd merge props into the inner table (via table.clone or a dictionary-join helper) to make it pass through.

Diagnostics + quick fixes

Yellow squigglies, one-click fixes:

Deprecation β€” toggle with luix.deprecationDiagnostics (default true):

  • Font = Enum.Font.GothamBold β†’ quick-fix replaces with FontFace = Font.fromName("Gotham", Enum.FontWeight.Bold).
  • TextColor = … (missing the trailing 3) β†’ quick-fix renames to TextColor3.

Prop validation β€” toggle with luix.propValidation.enabled (default true):

  • Unknown property on a known Roblox class β€” e("Frame", { ScrollingDirection = … }) warns "Unknown property ScrollingDirection on Frame. Did you mean Position?" with a Rename to Position quick-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.X warns because BorderMode expects Enum.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 β€” Position set to UDim2.fromScale(0.5, …) / (1, …) / etc. with no AnchorPoint flagged with an Info-level "add AnchorPoint = 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.
  • TextScaled gotcha β€” TextScaled = true with a pure-scale Size (or no Size) collapses text to zero; flagged with a clear explanation.

Optional WCAG color-contrast warnings (luix.contrastWarnings.enabled):

  • Walks the element tree and flags any TextColor3 whose contrast ratio against the nearest ancestor's BackgroundColor3 is 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…>" without RichText = true in the same props table warns that the tags will render as literal text, with a Set RichText = true quick-fix.

Auto-import (opt-in)

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.

Reference CodeLens

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).

Frame-stats CodeLens (off by default)

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 (default false).
  • luix.frameStatsLens.minDescendants (default 5) β€” only show the lens for elements with at least this many descendants. Stops trivial elements from cluttering the gutter.

Workspace-wide diagnostic summary (off by default)

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).

Sidebar (Activity Bar)

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:".

Design tokens β€” color, spacing, fonts

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).

Roblox font catalogue β€” family + weight autocomplete

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.

Snippets

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

Custom component annotations

Two forms work, and they compose:

---@extends Frame
---@prop gamepassId number
---@prop layoutOrder number?
local function GamepassCard(props): React.ReactNode
    return e("Frame", { … })
end
type GamepassCardProps = {
    gamepassId: number,
    layoutOrder: number?,
}

local function GamepassCard(props: GamepassCardProps)
    return New "Frame" { … }
end

The ---@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.


Configuration

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.

Framework toggles

{
  // 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"]
}

Per-class prop overrides

{
  "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"]
    }
  }
}

Editor integrations

{
  "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"
}

Auto-import (opt-in)

{
  "luix.autoImport.enabled": false,
  "luix.autoImport.style":   "relative",  // or "alias"
  "luix.autoImport.aliases": [
    { "filesystemPath": "src/Client/UI/Components", "robloxPath": "script.Components" }
  ]
}

Background / performance

  • luix.indexPersistence.enabled (default true) β€” 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 (default false) β€” 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.

Known limitations

  • 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 like Props<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 via luix.props if 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.

Development

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.svg

Press F5 from this folder in VS Code to launch an Extension Development Host with Luix loaded.

About

One VS Code extension that gives React-Luau, Roact, Fusion, and Vide users the same prop hints, hover docs, color pickers, RichText tooling, and asset previews.

Topics

Resources

License

Stars

Watchers

Forks

Packages