Skip to content

Custom UI Language

Lua scripts can pass the language directly to neon.register_draggable_ui_feature, provide values for bindings, find nodes by ID, change node properties, and connect click handlers.

Complete Lua example

lua
local ui_source = [[
box {
    width: 100%;
    height: 100%;

    rect {
        id: "draggable-root";
        draggable: true;
        width: 180px;
        color: rgba(15, 15, 20, 0.92);
        corner-radii: 8;
        layout: vertical;

        text {
            text: #title;
            color: white;
            font-size: 10;
            margin: 7;
        }

        for row in #rows {
            box {
                layout: horizontal;
                justify: space-between;
                padding: 4 7;

                text { text: #row.name; color: gray; font-size: 8; }
                text { text: #row.value; color: #accent; font-size: 8; }
            }
        }

        rect {
            id: "button";
            height: 20px;
            margin: 7;
            color: rgba(255, 255, 255, 0.08);
            hover-color: rgba(255, 255, 255, 0.16);
            corner-radii: 5;
            align: center;
            justify: center;

            text { text: #button-label; color: white; font-size: 8; }
        }
    }
}
]]

local feature = neon.register_draggable_ui_feature(
    "UI Example",
    "A script-provided draggable UI",
    ui_source
)

local clicks = 0

feature.provide_property("title", "Lua UI")
feature.provide_property("accent", types.color.new(214, 65, 112))
feature.provide_property("button-label", function()
    return "Clicked " .. clicks .. " time(s)"
end)
feature.provide_property("rows", function()
    return {
        { name = "Player", value = "Example Name" },
        { name = "Clicks", value = tostring(clicks) }
    }
end)

feature.connect("button", function(node)
    clicks = clicks + 1
    node.set_property("opacity", clicks % 2 == 0 and 1.0 or 0.8)
end)

The source is parsed when the feature is registered. Invalid syntax or a missing draggable-root makes registration fail.

Registering a draggable UI feature

neon.register_draggable_ui_feature

lua
local feature = neon.register_draggable_ui_feature(name, description, ui_source)
ArgumentTypeMeaning
namestringFeature name shown in Neon.
descriptionstringFeature description.
ui_sourcestringComplete custom UI language source text.

The returned object includes all normal types.feature methods and the UI-specific methods below.

Every draggable feature must contain exactly one node with:

text
id: "draggable-root";

The feature automatically adds X Position and Y Position settings. The root can be dragged while the Minecraft chat screen is open.

feature.provide_property

lua
feature.provide_property(name, value)
feature.provide_property(name, function()
    return current_value
end)

Provides data used by #name, ${name}, expressions, and for blocks. A function is called once per render binding pass and should take no arguments.

Supported Lua values are:

Lua valueUI value
nilMissing value
booleanBoolean
numberNumber
stringString
types.colorColor
Sequential tableList, usable by for
Key/value tableMap, usable as #item.field inside for
UserdataOriginal Java object, used for values such as textures

feature.get_node

lua
local node = feature.get_node("button")

Returns a types.ui_node for a parsed node ID. IDs should be unique in a scene. Nodes generated inside a for block are dynamic and should not be looked up by ID.

feature.connect

lua
feature.connect("button", function(node)
    print("Clicked " .. node.id)
end)

Connects a click handler by ID. The callback receives the clicked node.

For Lua callbacks, prefer feature.connect or node.on_click over the DSL on-click property.

Lua UI node API

The object returned by feature.get_node and click callbacks exposes these read-only fields:

FieldTypeMeaning
idstring or nilDSL node ID.
x, ynumberPosition relative to the parent.
width, heightnumberCurrent bounds.
absolute_x, absolute_ynumberCurrent screen position.
hoveredbooleanWhether the node is currently hovered.
activebooleanWhether the node is currently pressed/active.
focusedbooleanWhether the node owns keyboard focus.

node.set_property

lua
node.set_property("color", types.color.RED)
node.set_property("width", "75%")
node.set_property("display", "none")

Applies any property supported by that node type. Use strings for values with units or keywords. Lua booleans and numbers can be passed directly.

node.on_click

lua
local button = feature.get_node("button")
button.on_click(function(node)
    node.set_property("opacity", 0.5)
end)

Connects a click handler directly to the node.

Language syntax

Nodes and properties

text
node-type {
    property-name: value;

    child-type {
        property-name: value;
    }
}

Supported node types are box, rect, image, circle, text, and text-input. image is an alias of rect.

Properties end with ;. Braces delimit children. Property and node names are case-insensitive where their value parser says so, but lowercase is recommended. The parser supports line comments:

text
// This comment continues to the end of the line.

Strings use double quotes. Quotes do not currently have an escape syntax, so a literal " cannot be embedded in a quoted value.

Sizes

Size and position properties accept:

text
width: 120;     // fixed UI units
width: 120px;   // same as 120
width: 75%;     // percentage of parent
width: auto;    // content/layout controlled; width and height only

min-width and min-height accept fixed or percentage values. Numeric spacing properties such as padding and margin do not accept % or px.

Edge shorthand

padding and margin accept one, two, or four numbers:

text
padding: 8;           // all sides
padding: 6 10;        // vertical, horizontal
padding: 4 6 8 10;    // top, right, bottom, left

Colors

text
color: white;
color: transparent;
color: #f4a;
color: #ff44aa;
color: #80ff44aa;
color: rgb(255, 68, 170);
color: rgba(255, 68, 170, 0.5);
color: hsb(0.92, 0.73, 1.0);
color: hsba(0.92, 0.73, 1.0, 0.5);

Named colors are white, black, red, green, blue, yellow, cyan, magenta, gray, orange, and transparent.

RGB channels use 0 through 255. HSB channels and alpha use 0.0 through 1.0. Eight-digit hex is ARGB, not RGBA.

Direct bindings

A value consisting only of #name preserves its provided type:

text
text: #status;
color: #accent;
display: #visibility;

Nested values in loop maps use dot paths such as #player.name.

String interpolation

${name} inserts a provided value into surrounding text:

text
text: "Speed: ${speed} blocks/s";

This ${...} syntax is also used for component parameter substitution. In component declarations it is parse-time substitution.

Expressions

Expressions support numbers, quoted strings, variables, parentheses, and:

OperatorMeaning
+Numeric addition, or string concatenation if either value is a string
-Numeric subtraction
*Numeric multiplication
/Numeric division; division by zero produces 0

Standard precedence applies: multiplication and division run before addition and subtraction.

text
width: #health * 1.5 + 10;
height: (#rows * 12) + 8;
text: "Count: " + #count;
color: rgba(255, 255, 255, #opacity * 0.8);
color: hsba(#hue, 0.8, 1.0, #opacity);

The expression color functions are rgb, rgba, hsb, and hsba.

Loops

Use a Lua sequential table as the loop source:

text
for item in #items {
    box {
        layout: horizontal;
        text { text: #item.name; }
        text { text: #item.value; }
    }
}
lua
feature.provide_property("items", function()
    return {
        { name = "FPS", value = "144" },
        { name = "Ping", value = "32 ms" }
    }
end)

Reusable components

Declare a component before using it:

text
component Label {
    text {
        text: ${value};
        color: ${color};
        font-size: 9;
    }
}

box {
    Label { value: "First"; color: red; }
    Label { value: "Second"; color: white; }
}

Component arguments are token substitutions. They are not defaulted and are not Lua values unless the substituted result itself contains a binding.

Common properties

Every node type supports these properties:

PropertyValuesDescription
idstringUnique lookup/connection ID.
width, heightsizeFixed, %, px, or auto.
min-width, min-heightsizeFixed or percentage minimum.
aspect-rationumberWidth-to-height ratio.
positionrelative, absoluteYoga position mode; any value other than absolute is relative.
left, top, right, bottomsizeFixed or percentage position offset.
layout, flex-directionrow, horizontal, column, vertical, row-reverse, column-reverseChild layout direction.
justify, justify-contentstart, flex-start, center, end, flex-end, between, space-between, around, space-aroundMain-axis child alignment.
align, align-itemsstart, flex-start, center, end, flex-end, stretchCross-axis child alignment. On text, this controls text alignment instead.
align-selfsame as alignOverrides this node's cross-axis alignment.
flexnumberYoga flex shorthand.
flex-grownumberRemaining-space growth factor.
flex-shrinknumberShrink factor.
gapnumberGap between children.
marginedge shorthandOuter spacing.
margin-top, margin-right, margin-bottom, margin-leftnumberIndividual outer spacing.
paddingedge shorthandInner spacing.
padding-top, padding-right, padding-bottom, padding-leftnumberIndividual inner spacing.
hover-colorcolorReplaces the node's base color while hovered.
active-colorcolorReplaces the node's base color while active.
draggablebooleanMakes the node absolutely positioned and pointer-draggable. Script feature roots are only draggable in chat.
focusablebooleanAllows the node to own keyboard focus. text-input enables this automatically.
overflowvisible, hidden, scrollChild clipping or vertical wheel scrolling. Unknown values mean visible.
displaynone, any other valueRemoves/adds the node from layout.
opacitynumberMultiplies the rendered node color alpha; it does not recursively change child opacity.
hover-exclusivebooleanStops click/hover propagation above this node.
on-clickcallback objectLua should use connect or on_click.

The default flex direction is vertical/column and the default cross-axis alignment is stretch.

box

box is a layout-only node. It supports all common properties and renders no shape of its own.

rect and image

PropertyValuesDefaultDescription
colorcolorwhiteFill/tint color.
background-blurboolean or number0true means full blur (1); a number controls blur opacity.
drop-shadow-colorcolornoneRenders the rectangle into the shadow canvas.
corner-radiione or four numbers0All corners, or top-left/top-right/bottom-right/bottom-left.
outline-thicknessnumber0Outline width.
outline-colorcolorblackOutline color.
textureresource path, texture userdatanoneImage/texture. Lua bindings can provide texture userdata.
samplernearest, linearrenderer defaultnearest selects nearest filtering; every other string selects linear.
custom-uvsfour numbersnoneCustom texture UV/inset vector.

circle

PropertyValuesDefaultDescription
colorcolorwhiteFill/tint color.
outline-thicknessnumber0Outline width.
outline-colorcolorblackOutline color.
progressnumber1Rendered circle progress/fraction.
textureresource path, texture userdatanoneOptional texture.

The rendered diameter uses the node's width.

text

PropertyValuesDefaultDescription
textstringemptyDisplayed text.
colorcolorwhiteFirst/base text color.
gradient-colorcolornoneOptional second gradient color.
font-sizenumber10Text size.
font-facefont IDgoogle_sans_flex_mediumFont selection.
line-heightnumber1Line-spacing multiplier.
baseline-offsetnumber0Vertical baseline adjustment.
wrapbooleanfalseEnables word wrapping to available width.
align, align-itemsleft, center, rightleftHorizontal text alignment.
vertical-aligntop, center, bottomcenterVertical text alignment inside fixed height.
scrollbooleanfalseHorizontally scrolls overflowing text.
scroll-speednumber35Scrolling speed.
scroll-gapnumber15Gap before repeated scrolling text.
shadowbooleanfalseEnables text shadow.
shadow-offsetone or two numbers2.5 2.5Both axes, or X and Y offsets.
shadow-offset-x, shadow-offset-ynumber2.5Individual shadow offsets.

Font IDs:

  • tahoma
  • stratum2
  • museo_sans_500
  • google_sans_flex_regular
  • google_sans_flex_medium
  • google_sans_flex_semibold
  • noto_sans_hebrew
  • lucide_icons

An unknown font ID falls back to google_sans_flex_regular.

text-input

PropertyValuesDefaultDescription
textstringemptyEditable text.
placeholderstringemptyText shown while empty.
font-sizenumber10Text size.
font-facefont IDgoogle_sans_flex_mediumFont selection.
baseline-offsetnumber0Vertical baseline adjustment.
colorbound types.colorwhiteInput text color.
placeholder-colorbound types.colorgrayPlaceholder color.
cursor-colorbound types.colorwhiteCaret color.
selection-colorbound types.colortranslucent blueSelection highlight color.

text-input supports typing, selection, Backspace/Delete, arrows, Home/End, and Ctrl+A/C/V/X. Its color fields currently accept typed color bindings or node.set_property with types.color; static color strings are not applied.

Notes

  • Keep IDs unique.
  • Bind functions should be fast because they run during UI updates.
  • Use a sequential Lua table for loops; sparse numeric tables are not supported.
  • Dynamic loop nodes are rebuilt/reused by index. Do not keep their node IDs.
  • hover-color, active-color, and opacity operate on the node's rendered color, not all descendants.
  • A drop-shadow-color has an effect only when the owning feature participates in the shadow render pass, which draggable Lua features do automatically.