React Clean UI

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 game

When 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-ripple

3. 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.git

4. 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\Stories

5. 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 -w

Terminal 2 — Compile your game

cd C:\Users\username\Documents\dev-packages
npx rbxtsc -w

Terminal 3 — Run Rojo

cd C:\Users\username\Documents\dev-packages
rojo serve

8. 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 fileTest file
src/Components/Input/Increment.tsxsrc/Tests/Components/Input/Increment.test.tsx
src/Components/Input/Increment.step.tssrc/Tests/Components/Input/Increment.step.test.ts
src/Providers/modal.provider.tsxsrc/Tests/Providers/modal.provider.test.tsx

The suite currently covers these areas:

  • ChartBarChart, Pie
  • DecoratorBoxShadow, Corners, Gradient, Padding
  • InputButton, Checkbox, HoverButton, Increment (plus its step logic), Input (plus its validation logic), Select, Slider, Switch
  • InteractionModal, Toast, Tooltip
  • LayoutAccordion, Column, Container, Draggable, Droppable, Fieldset, FlexItem, Grid, Group, HStack, Pagination, Row, Scroller, Table, Tabs, VStack
  • NavigationMenu
  • SurfaceBox, Card, Icon, ProgressBar
  • TypographyText
  • Providersmodal.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 @Test method 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) mounts element under a ScreenGui in CoreGui, wrapped in a ThemeProvider using DefaultTheme.
  • withMounted(width, height, element, fn) mounts the element, runs fn against it, and always unmounts afterwards, even when an assertion fails.
  • waitForLayout, waitForGuiObject and waitForDescendant poll 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 (or npm run build / npm run watch, as in Terminal 1 above) inside Packages/rbxts-react-clean-ui. The runner's own compile step only builds the dev-packages root, so skipping this step runs stale code.
  • Make sure rojo serve is 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 ...]
OptionDescription
--studioRun in Roblox Studio (default).
--luneRun 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.
--jsonPrint a machine-readable summary.
--helpList every option.
Exit codeMeaning
0All tests passed or were skipped.
1One or more tests failed.
2The tests couldn't be run.
130The 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
GitHub Repository

On this page