Quick Start#

Install OneJS and build a working UI component in your Unity project. The whole journey, from install to hot reload, in under a minute:

Install#

Requirements: Unity 6.3 or newer, and Node.js 18 or newer with npm on your PATH. JSRunner runs npm install and esbuild for you, so it needs Node even though you never type an npm command.

Open Window > Package Management > Package Manager, click + > Install package from git URL..., and enter:

https://github.com/Singtaa/OneJS.git

Or clone directly into your Assets folder:

cd YourProject/Assets
git clone --depth 1 https://github.com/Singtaa/OneJS.git

Prefer the git URL above: a clone in Assets compiles OneJS's tests and warnings into your project.

Or get OneJS on the Asset Store: the same package plus a Premade folder with three complete onejs-ui themes and sample packs.

Or hand it to an AI coding agent: AI Agent Setup is the same install and setup as a sequence of terminal commands, with no inspector clicks.

Set Up a Project#

  1. Create a new GameObject, rename it App, and add the JSRunner component
  2. Click Initialize Project in the inspector
  3. Enter Play Mode

Initialize Project installs the npm packages and builds the bundle (a few seconds the first time). The starter UI appears in the Game view straight away as an edit-mode preview; Play mode runs it for real and keeps the file watcher going.

Your project folder lives next to your scene, named after the GameObject:

Assets/Scenes/YourScene/
└── App/                      # Named after your GameObject
    ├── ~/                    # Working directory (~ = Unity ignores)
    │   ├── index.tsx         # Entry point
    │   ├── package.json
    │   ├── tsconfig.json
    │   └── esbuild.config.mjs
    ├── app.js.txt            # Built bundle
    └── PanelSettings.asset   # Project marker

(abridged; the full layout is in Project Setup)

Write a Component#

Click Open Code Editor in JSRunner's inspector (the ~ folder is hidden in Unity's Project window) and replace the contents of index.tsx:

import { render, View, Label, Button } from "onejs-react"
import { useState } from "react"

function Counter() {
    const [count, setCount] = useState(0)

    return (
        <View style={{ padding: 20, alignItems: "flex-start" }}>
            <Label text={`Count: ${count}`} style={{ fontSize: 24, marginBottom: 10 }} />
            <Button text="Increment" onClick={() => setCount(c => c + 1)} />
        </View>
    )
}

render(<Counter />, __root)

Save the file. JSRunner automatically watches for changes, rebuilds, and hot-reloads. The UI updates in the Game view without leaving the editor. This is standard React: useState, JSX, component composition. The difference is that <Button> creates a real UnityEngine.UIElements.Button rendered on the GPU.

render is the primitive every OneJS app is built on. For a game, oj offers mount instead: render plus a stage (the window, in logical pixels) and a runtime. It is the recommended entry point there, and it works in any OneJS project.

Calling C##

Access any C# API through the CS global:

function DebugButton() {
    const handleClick = () => {
        CS.UnityEngine.Debug.Log("Hello from JS!")
        console.log(`Time: ${CS.UnityEngine.Time.time}`)
    }

    return <Button text="Debug" onClick={handleClick} />
}

This works with all Unity APIs, your own game code, and third-party libraries.

The Ball Drop#

The demo from the video above: one file that sets up a scene and wires a button to it. UI and game code, side by side.

import { render, Button } from "onejs-react"
import {
    Camera, Collider, Color, GameObject, MeshRenderer,
    Physics, PhysicsMaterial, PrimitiveType, Random, Rigidbody, Vector3
} from "UnityEngine"

const bouncy = new PhysicsMaterial()
bouncy.bounciness = 0.8
let ground: GameObject | null = null

export function onPlay() {
    Physics.gravity = new Vector3(0, -20, 0)
    ground = GameObject.CreatePrimitive(PrimitiveType.Plane)
    ground.GetComponent(MeshRenderer).material.color = Color.yellow
    ground.GetComponent(Collider).material = bouncy
    Camera.main.transform.position = new Vector3(9, 4, -8)
    Camera.main.transform.LookAt(new Vector3(0, 1, 0))
}

export function onStop() {
    if (ground) GameObject.Destroy(ground)
    ground = null
}

function dropBall() {
    const ball = GameObject.CreatePrimitive(PrimitiveType.Sphere)
    ball.transform.position = new Vector3(Random.Range(-2, 2), 6, 0)
    ball.GetComponent(MeshRenderer).material.color = Color.red
    ball.GetComponent(Collider).material = bouncy
    ball.AddComponent(Rigidbody)
}

render(<Button text="Drop Ball" onClick={dropBall} />, __root)

Three notes. The UnityEngine import works because the built-in esbuild plugin turns ES6 imports of C# namespaces into CS.* references at build time. Scene setup lives in onPlay because module-level code also runs in the edit-mode preview, where it would edit your scene (see Lifecycle Hooks). And physics only steps in Play Mode, so enter Play Mode before clicking the button.

Quick Prototyping with JSPad#

For quick experiments without file setup, use JSPad instead:

  1. Add the JSPad component to a GameObject
  2. Write TSX directly in the inspector
  3. Click Build, then enter Play Mode

JSPad is self-contained. The bundle lives in the scene file. Great for learning and quick tests. Use JSRunner for real projects.

Coming from V2?#

V2V3
ScriptEngine + Runner + BundlerSingle JSRunner component
Project-root App/ folderScene Folder convention
Shared PanelSettingsPer-instance (auto-created)
npm run setup to initialize"Initialize Project" button
PreactReact 19
Separate Tailwind watcherBuilt-in (import "onejs:tailwind")

Next Steps#