Development
Set up a local development environment and contribute to React Clean UI.
This guide explains how to set up a local environment for developing React Clean UI.
Follow the guide directions careful, as this step method will allow you to preview your work using Stories in the UI Labs plugin.
This guide assumes you're already familiar with roblox-ts, Rojo, and Roblox Studio.
1. Create a development project
Create a new roblox-ts project outside of the React Clean UI repository.
mkdir C:\Users\username\Documents\dev-packages
cd C:\Users\username\Documents\dev-packages
npm init roblox-ts gameWhen prompted:
- Leave Project directory blank.
- Answer Y to the remaining questions.
2. Install the required packages
Install the packages required for the development project.
npm install @rbxts/react @rbxts/react-roblox @rbxts/ui-labs @rbxts/services @rbxts/react-ripple3. Clone React Clean UI
Create a /Packages directory and clone the repository into it.
mkdir Packages
cd Packages
git clone https://github.com/Shadercloud/rbxts-react-clean-ui.git4. Create a directory junction
In order to compile the Stories from the React Clean UI package, you must create a directory junction in your development project.
Run this from the root of your development project (dev-packages), not from inside Packages.
Using PowerShell:
cd ..
New-Item -ItemType Junction -Path "src\Stories" -Target "Packages\rbxts-react-clean-ui\Stories"or using Command Prompt:
cd ..
mklink /J src\Stories Packages\rbxts-react-clean-ui\Stories5. Configure TypeScript
Replace your root project's tsconfig.json with the following:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"module": "commonjs",
"moduleResolution": "Node",
"noLib": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"moduleDetection": "force",
"strict": true,
"target": "ESNext",
"typeRoots": [
"node_modules/@rbxts"
],
"types": [
"types"
],
"rootDir": "src",
"outDir": "out",
"baseUrl": "src",
"incremental": true,
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
"paths": {
"@rbxts/react": [
"../node_modules/@rbxts/react"
],
"@rbxts/react-roblox": [
"../node_modules/@rbxts/react-roblox"
],
"@rbxts/ui-labs": [
"../node_modules/@rbxts/ui-labs"
],
"ripple": [
"../node_modules/@rbxts/ripple"
],
"react-ripple": [
"../node_modules/@rbxts/react-ripple"
],
"@rbxts/services": [
"../node_modules/@rbxts/services"
],
"lunit": {
"$path": "node_modules/@rbxts/lunit"
},
"@rbxts/react-clean-ui": [
"../Packages/rbxts-react-clean-ui/out"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}6. Configure Rojo
You must install Rojo (easier method is by using the Rojo Extension for `VS Studio Code``)
Create a default.project.json file in the root of your development project.
{
"name": "React Clean UI Dev Environment",
"globIgnorePaths": [
"**/package.json",
"**/tsconfig.json"
],
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$className": "ServerScriptService",
"TS": {
"$path": "out/server"
}
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"rbxts_include": {
"$path": "include",
"node_modules": {
"$className": "Folder",
"@rbxts": {
"$className": "Folder",
"compiler-types": {
"$path": "node_modules/@rbxts/compiler-types"
},
"react": {
"$path": "node_modules/@rbxts/react"
},
"react-roblox": {
"$path": "node_modules/@rbxts/react-roblox"
},
"services": {
"$path": "node_modules/@rbxts/services"
},
"ui-labs": {
"$path": "node_modules/@rbxts/ui-labs"
},
"ripple": {
"$path": "node_modules/@rbxts/ripple"
},
"react-ripple": {
"$path": "node_modules/@rbxts/react-ripple"
},
"types": {
"$path": "node_modules/@rbxts/types"
},
"rbxts-react-clean-ui": {
"$path": "Packages/rbxts-react-clean-ui/out"
}
},
"@rbxts-js": {
"$path": "node_modules/@rbxts-js"
}
}
},
"TS": {
"$path": "out/shared"
},
"PackageStories": {
"$path": "out/Stories"
}
},
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"TS": {
"$path": "out/client"
}
}
},
"Workspace": {
"$className": "Workspace",
"$properties": {
"FilteringEnabled": true
}
},
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
},
"SoundService": {
"$className": "SoundService",
"$properties": {
"RespectFilteringEnabled": true
}
}
}
}7. Start the development environment
You'll need three terminals running simultaneously (start them in the specific order listed below).
Terminal 1 — Compile React Clean UI
cd C:\Users\username\Documents\dev-packages\Packages\rbxts-react-clean-ui
npm install
npx rbxtsc -wTerminal 2 — Compile your game
cd C:\Users\username\Documents\dev-packages
npx rbxtsc -wTerminal 3 — Run Rojo
cd C:\Users\username\Documents\dev-packages
rojo serve8. Open Roblox Studio
Open your development place in Roblox Studio and connect it to the running Rojo server.
9. Install UI Labs
Install the UI Labs plugin:
https://ui-labs.luau.page/docs/installation
Once installed, open Plugins → UI Labs. You should see the React Clean UI component stories, allowing you to preview components while you develop.
Testing
React Clean UI is tested with @rbxts/lunit, a class and decorator based test framework.
Test layout
Test files match **/*.{test,spec}.{ts,tsx} and live under src/Tests/, which mirrors the layout of src/:
| Source file | Test file |
|---|---|
src/Components/Input/Increment.tsx | src/Tests/Components/Input/Increment.test.tsx |
src/Components/Input/Increment.step.ts | src/Tests/Components/Input/Increment.step.test.ts |
src/Providers/modal.provider.tsx | src/Tests/Providers/modal.provider.test.tsx |
The suite currently covers these areas:
- Chart —
BarChart,Pie - Decorator —
BoxShadow,Corners,Gradient,Padding - Input —
Button,Checkbox,HoverButton,Increment(plus its step logic),Input(plus its validation logic),Select,Slider,Switch - Interaction —
Modal,Toast,Tooltip - Layout —
Accordion,Column,Container,Draggable,Droppable,Fieldset,FlexItem,Grid,Group,HStack,Pagination,Row,Scroller,Table,Tabs,VStack - Navigation —
Menu - Surface —
Box,Card,Icon,ProgressBar - Typography —
Text - Providers —
modal.provider
The compiled out/Tests/** output is excluded from the published package ("!out/Tests/**" in the files field of package.json).
Writing a test
- Each file contains one test class, exported with
export = ClassName(not a named export). - Each
@Testmethod has a short camelCase name and a@DisplayName("...")sentence describing the behaviour it checks. - Tests are split by tag, not by folder:
@Tag("Lune")for pure logic that runs headlessly.@Tag("Studio")for anything that mounts React or creates Instances.
import { Test, Assert, Tag, DisplayName } from "@rbxts/lunit";
import { resolveSteppedValue } from "../../../Components/Input/Increment.step";
@Tag("Lune")
class IncrementStepValidation {
@DisplayName("A step that would overshoot max is clamped to max")
@Test
public clampsToMax() {
Assert.equal(resolveSteppedValue(8, "increment", 5, 0, 10), 10);
}
}
export = IncrementStepValidation;Studio mount tests must parent their host Frame under a ScreenGui. Roblox never lays out a detached Frame, so AbsoluteSize and text bounds stay at 0. The test must also unmount the React root and Destroy() the host when it finishes.
Layout helpers
Rather than building a ScreenGui host by hand, Studio mount tests use the shared helpers in src/Tests/Helpers/layout.ts:
mountInScreenGui(width, height, element)mountselementunder aScreenGuiinCoreGui, wrapped in aThemeProviderusingDefaultTheme.withMounted(width, height, element, fn)mounts the element, runsfnagainst it, and always unmounts afterwards, even when an assertion fails.waitForLayout,waitForGuiObjectandwaitForDescendantpoll until the engine has laid out the instance you're checking.assert*layout assertions check real on-screen rects:assertContained,assertAllDescendantsContained,assertNoSiblingOverlap,assertTextFits,assertCenteredIn,assertSizeApprox,assertStackedVertically/assertStackedHorizontally, and others.
Studio tests also skip themselves outside Roblox with @Skip(!Runtime.isRoblox(), STUDIO_SKIP_MESSAGE):
import React from "@rbxts/react";
import { Test, Decorators, Runtime, Tag, DisplayName } from "@rbxts/lunit";
import { Box } from "../../../Components/Surface/Box";
import { STUDIO_SKIP_MESSAGE, assertCenteredIn, waitForGuiObject, withMounted } from "../../Helpers/layout";
const { Skip } = Decorators;
@Tag("Studio")
class BoxMountValidation {
@Skip(!Runtime.isRoblox(), STUDIO_SKIP_MESSAGE)
@DisplayName("center={true} with a fixed width and height places the Box in the middle of the host")
@Test
public centeredInHost() {
withMounted(400, 300, <Box center={true} width={200} height={100} />, (mounted) => {
const box = waitForGuiObject<ImageLabel>(mounted.host, "Box");
assertCenteredIn(box, mounted.host, "both", 1, "centered box");
});
}
}
export = BoxMountValidation;Running tests
Running the suite requires the Lunit Test Companion VS Code extension (publisher ShaderCloud). Once it's installed, you can run tests from VS Code's Test Explorer with the Run with Lune or Run in Roblox Studio profile, or with the Lunit: Run All Tests command.
Before a Studio run:
- Compile React Clean UI with
npx rbxtsc(ornpm run build/npm run watch, as in Terminal 1 above) insidePackages/rbxts-react-clean-ui. The runner's own compile step only builds thedev-packagesroot, so skipping this step runs stale code. - Make sure
rojo serveis running and syncing into Studio. - Studio caches required modules for the session. After you edit a component, restart Studio (or re-sync) before you trust a result.
Running from the terminal
For terminal or CI-style runs, use the extension's CLI. It runs the same code path as the Test Explorer. The extension writes a version-stable launcher to VS Code's global storage, so use that path instead of a versioned extension path.
Run this from the dev-packages root (the Rojo project that syncs into Studio), using Git Bash:
cd C:\Users\username\Documents\dev-packages
node "$APPDATA/Code/User/globalStorage/shadercloud.vscode-lunit-companion/lunit-cli.js" --studio --workspace "$(pwd -W)" [filter ...]| Option | Description |
|---|---|
--studio | Run in Roblox Studio (default). |
--lune | Run headlessly with Lune. Studio-tagged files log module not found: @rbxts/react load warnings under Lune. These are expected and harmless. |
[filter ...] | Case-insensitive substrings matched against the file path, class, method, or display name, e.g. Grid or Increment. |
--json | Print a machine-readable summary. |
--help | List every option. |
| Exit code | Meaning |
|---|---|
0 | All tests passed or were skipped. |
1 | One or more tests failed. |
2 | The tests couldn't be run. |
130 | The run was cancelled. |
Other checks
Run these from Packages/rbxts-react-clean-ui alongside the test suite:
npm run typecheck
npm run lint
npm run format:check