-
Notifications
You must be signed in to change notification settings - Fork 0
React Builders
@nrbx/react-builders is a fluent, chainable API for building Roblox UI
trees without writing JSX. It is modeled on Discord.js's component builders:
you construct a builder object, call setter methods to populate its
properties, nest children, and then either render the result as a React
element or serialize it to a plain table for transport.
It is useful when you want to:
- Build UI trees from data, without a JSX file per layout
- Generate a UI on the server and send it to clients over a network boundary
- Store component layouts as data (for example, in a DataStore or config file)
- Compose a UI programmatically where method chaining reads better than nested JSX
npm install @nrbx/react-builders
yarn add @nrbx/react-builders
pnpm add @nrbx/react-buildersThe package depends on @nrbx/react as a peer dependency, so make sure both
are installed.
import { Frame, TextLabel, Builders } from "@nrbx/react-builders";
const panel = new Frame()
.setBackground(Color3.fromRGB(255, 255, 255))
.setSize(new UDim2(0, 200, 0, 100))
.addChildComponent((child) =>
child.setType("TextLabel").setText("Hello world").setTextSize(24),
);
// Render it
const element = Builders.constructElement(panel);
root.render(element);Every setter returns the builder itself, so calls chain in any order.
A builder is just a Roblox class name plus a map of property values and a
list of child builders. It holds no Roblox Instances and performs no work
until you call constructElement (or toTable). This is what makes the
tree serializable.
Typed .set() — accepts any writable property of the pinned class:
new Frame().set("Size", new UDim2(0, 200, 0, 100));
new Frame().set("BackgroundColor3", new Color3(1, 0, 0));The first argument is narrowed to the properties that exist on the builder's class, and the second argument is checked against that property's type.
Shorthand setters — ergonomic methods for the most common properties:
new Frame()
.setBackground("white")
.setSize(new UDim2(0, 200, 0, 100))
.setPosition(new UDim2(0, 10, 0, 10))
.setVisible(true)
.setZIndex(5);Shorthand setters exist on every builder regardless of class. The
class-specific builders add more (for example, ScrollingFrame adds
setCanvasSize, CanvasGroup adds setGroupColor).
The color setters (setBackground, setTextColor, setBorderColor,
setImageColor, and setGroupColor) accept either a Color3 or a string,
and the string forms are resolved at build time:
| Form | Example |
|---|---|
| Named CSS color |
"white", "rebeccapurple", "tomato"
|
| Hex |
"#fff", "#ff0000", "ff0000"
|
rgb() / rgba()
|
"rgb(255, 0, 0)", "rgba(255, 0, 0, 1)"
|
Alpha values in rgba() are ignored; Roblox transparency is set separately
with setBackgroundTransparency.
Nest builders with addChild, addChildComponent (an alias), or
addChildren for several at once:
new Frame().addChild(
new TextLabel().setText("Direct instance"),
);
new Frame().addChildComponent((child) =>
child.setType("TextButton").setText("Callback style"),
);
new Frame().addChildren(
new TextLabel().setText("A"),
new TextLabel().setText("B"),
);The callback form receives a fresh Frame builder; call setType on it to
change the class. Callbacks are the idiomatic style because they keep the
parent chain readable.
Set a key for list reconciliation; it becomes the key prop on the generated
element:
list.addChild(new TextLabel().setText("Item").setKey("item-1"));Builders.constructElement walks the tree depth-first and produces a React
element via React.createElement:
const element = Builders.constructElement(panel);
root.render(element);Any builder class name is passed through as a Roblox class tag (for example
"Frame", "TextLabel"), so it renders exactly like the equivalent JSX.
You can also deserialize and render in one call:
const element = Builders.createElementFromTable(node);
root.render(element);If you already have a JSX component and want it inside a builder tree, use
wrapElement:
const button = <MyFancyButton label="OK" />;
const panel = new Frame().addChild((child) =>
Builders.wrapElement(button, child),
);The wrapped element is appended to that builder's children at render time.
Register a React component so builders can reference it by string key:
import { Builders } from "@nrbx/react-builders";
import { MyCard } from "./components/MyCard";
Builders.registerComponent("MyCard", MyCard);
const card = new Builders.Builder("MyCard")
.set("title", "Hello world")
.set("subtitle", "A card built from data");When constructElement encounters a registered key, it calls the registered
component with the stored properties instead of instantiating a Roblox class.
Unregister with unregisterComponent and list keys with
getRegisteredComponents.
The whole point of a table-based builder is that the tree is data. Use
toTable to get a JSON-safe structure, send it over the network, and
rebuild it on the other side:
// Server
const tree = new Frame()
.setSize(new UDim2(1, 0, 1, 0))
.setBackground("#0f172a")
.addChildComponent((child) =>
child.setType("TextLabel").setText("From the server").setTextColor("white"),
);
remote.FireClient(player, tree.toTable());// Client
remote.OnClientEvent.Connect((node) => {
const builder = Builders.fromTable(node);
const element = Builders.constructElement(builder);
root.render(element);
});toTable converts every Roblox value to a JSON-safe representation:
| Roblox type | Serialized shape |
|---|---|
Color3 |
{ type: "Color3", r, g, b } |
UDim2 |
{ type: "UDim2", xScale, xOffset, yScale, yOffset } |
UDim |
{ type: "UDim", scale, offset } |
Vector2 |
{ type: "Vector2", x, y } |
Rect |
{ type: "Rect", min, max } |
EnumItem |
{ type: "Enum", enumName, value } |
string, number, boolean
|
passed through unchanged |
fromTable reverses this, reconstructing real Color3, UDim2, and
EnumItem values. A BuilderNode looks like:
{
type: "Frame",
properties: {
Size: { type: "UDim2", xScale: 1, xOffset: 0, yScale: 1, yOffset: 0 },
BackgroundColor3: { type: "Color3", r: 0.058, g: 0.09, b: 0.164 },
},
children: [
{
type: "TextLabel",
properties: { Text: "From the server" },
children: [],
},
],
}These classes pin the builder type to a specific Roblox class so .set() is
fully type-checked, and add class-specific shorthand methods.
| Class | Roblox class |
|---|---|
Frame |
Frame |
TextLabel |
TextLabel |
TextButton |
TextButton |
TextBox |
TextBox |
ImageLabel |
ImageLabel |
ImageButton |
ImageButton |
ScrollingFrame |
ScrollingFrame |
CanvasGroup |
CanvasGroup |
ViewportFrame |
ViewportFrame |
UIListLayout |
UIListLayout |
UIGridLayout |
UIGridLayout |
UIPageLayout |
UIPageLayout |
UITableLayout |
UITableLayout |
UIPadding |
UIPadding |
UICorner |
UICorner |
UIStroke |
UIStroke |
UIGradient |
UIGradient |
UIAspectRatioConstraint |
UIAspectRatioConstraint |
UISizeConstraint |
UISizeConstraint |
UITextSizeConstraint |
UITextSizeConstraint |
For anything else, use the generic builder with a class name string:
new Builders.Builder("UIListLayout").set("FillDirection", Enum.FillDirection.Vertical);The namespace is the one-stop import for the whole API:
import { Builders } from "@nrbx/react-builders";
const frame = new Builders.Frame();
const label = new Builders.TextLabel().setText("Hi");
const generic = new Builders.Builder("Frame");Full contents:
| Member | Purpose |
|---|---|
Builder |
Generic builder for any class name |
Frame, TextLabel, ... |
Concrete builder classes |
constructElement |
Builder tree → React element |
createElementFromTable |
fromTable + constructElement in one call |
fromTable |
Reconstruct a builder from a BuilderNode
|
wrapElement |
Embed an existing React element as a child |
registerComponent |
Register a custom component by key |
unregisterComponent |
Remove a registered component |
getRegisteredComponents |
List registered component keys |
| Method | Description |
|---|---|
set(key, value) |
Typed property setter |
setProperty(key, value) |
Untyped property setter (internal) |
setType(name) |
Change the Roblox class name |
setBackground(value) |
BackgroundColor3, accepts color string |
setBackgroundTransparency(value) |
BackgroundTransparency |
setSize(value) |
Size (UDim2) |
setPosition(value) |
Position (UDim2) |
setAnchorPoint(value) |
AnchorPoint (Vector2) |
setVisible(value) |
Visible |
setZIndex(value) |
ZIndex |
setLayoutOrder(value) |
LayoutOrder |
setRotation(value) |
Rotation |
setTransparency(value) |
Transparency |
setClipsDescendants(value) |
ClipsDescendants |
setAutomaticSize(value) |
AutomaticSize |
setText(value) |
Text |
setTextColor(value) |
TextColor3, accepts color string |
setTextSize(value) |
TextSize |
setTextTransparency(value) |
TextTransparency |
setFont(value) |
Font |
setTextWrapped(value) |
TextWrapped |
setTextXAlignment(value) |
TextXAlignment |
setTextYAlignment(value) |
TextYAlignment |
setTextTruncate(value) |
TextTruncate |
setRichText(value) |
RichText |
setTextScaled(value) |
TextScaled |
setBorderSizePixel(value) |
BorderSizePixel |
setBorderColor(value) |
BorderColor3, accepts color string |
setImage(value) |
Image |
setImageColor(value) |
ImageColor3, accepts color string |
setImageTransparency(value) |
ImageTransparency |
setScaleType(value) |
ScaleType |
setSliceCenter(value) |
SliceCenter (Rect) |
setSliceScale(value) |
SliceScale |
setActive(value) |
Active |
setSelectable(value) |
Selectable |
setAutoButtonColor(value) |
AutoButtonColor |
setModal(value) |
Modal |
addChild(child) |
Append a builder or callback |
addChildComponent(child) |
Alias of addChild
|
addChildren(...children) |
Append several builders |
setKey(key) |
Set the React key |
get(key) |
Read a stored property |
has(key) |
Whether a property is set |
clone() |
Deep copy the tree |
merge(source) |
Bulk-set properties from a plain object |
toTable() |
Serialize to a BuilderNode
|
setBackground and friends route through resolveColorValue, which handles
the color strings shown in the table above.
-
No events in serialized trees. Event handlers are functions, and
functions cannot be JSON-encoded. Build the tree on the client (or
reattach events after
fromTable) when you need interactive behavior. You can still set events on client-built trees by using.set()with an event callback value before rendering. -
Properties are applied verbatim.
constructElementpasses stored properties straight intoReact.createElement; it does not validate that a property exists on the Roblox class at runtime. The TypeScript layer catches mistakes for known classes, but the genericBuilderis untyped by design. -
Registered components must be registered before render. If you
serialize a tree that references a custom component key and send it to
another machine, that machine must register the same key before calling
fromTable+constructElement. -
Unsupported value types are dropped.
toTablereturnsundefinedfor value types it does not recognize, which JSON then omits. Stick to the supported Roblox value types listed above.