Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Project Structure & Build

This page covers everything around a superui UI that isn’t the components themselves: where files live, how to get editor support for .tsx, the three ways a UI gets built, and how hot reload works.

The UI directory

Each UI is a directory somewhere under your Bevy assets/ folder, laid out like a tiny web page. The path is entirely your choice — you pass it to from_asset_dir — but the examples group UIs under assets/ui/<name>/, and this guide follows that convention:

assets/ui/counter/
├── index.html   ← entry point / manifest
├── style.css    ← styles
└── app.tsx      ← components + render() call

index.html is the manifest

index.html is the single entry superui loads. It links the other files exactly like a browser would:

<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <script type="module" src="app.tsx"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  • <link rel="stylesheet"> pulls in the CSS.
  • <script type="module" src="…"> names your component module. Point it at app.tsx for a supersolid UI.
  • The <body> provides the mount target (#root) that your render() call looks up.

Because index.html references everything else, mounting only needs that one path:

commands.spawn(SuperUiRoot::from_asset_dir("ui/counter", &assets));

from_asset_dir("ui/counter", …) loads assets/ui/counter/index.html and bundles a full-viewport root Node so percentage-based and inset children resolve against the whole window. If you need a custom root node, use SuperUiRoot::from_asset_dir_with(dir, node, &assets) instead.

Editor support for .tsx

Your .tsx is real TypeScript as far as the editor is concerned, so you get autocomplete, hover docs, and type-checking — but only once the supersolid types are resolvable. superui projects them into your project with a CLI:

cargo superui install

This writes a gitignored superui_modules/ directory containing the ambient type declarations and a tsconfig.json that maps the bare supersolid import to them:

{
  "compilerOptions": {
    "jsx": "preserve",
    "moduleResolution": "bundler",
    "lib": ["ESNext", "DOM"],
    "paths": {
      "supersolid": ["./superui_modules/supersolid/index.d.ts"]
    }
  },
  "include": ["superui_modules/**/*.d.ts", "assets/**/*.tsx"]
}

Nothing here affects the build or runtime — superui’s Rust transpiler is the real consumer of your .tsx. The tsconfig exists purely so your editor understands the code (jsx: "preserve" leaves JSX untouched; there is no React runtime). superui_modules/ is regenerated by the command, so keep it out of version control.

Build modes

The same .tsx runs three ways, and which one you get depends on how you build:

CommandSource of truthHot reload
cargo run --features hmrlive .tsx, transpiled on loadyes
cargo runpre-built .superui/build/*.jsno
cargo build --target wasm32-unknown-unknownpre-built .superui/build/*.jsno

Live .tsx with hot reload

Enable an hmr feature in your crate:

[features]
hmr = ["superui/hmr", "bevy/file_watcher"]

Then cargo run --features hmr transpiles .tsx as it loads and watches the files. Editing a component rebuilds that UI in place. See Hot reload below.

Pre-transpiled JS (release native and web)

Without hmr, the transpiler is not in the running binary — so it can’t live-load .tsx, and on the web the transpiler (which uses native-only tooling) must not be compiled into the wasm binary at all. For these builds, transpile your .tsx ahead of time in a build script. Add a build.rs:

//! Pre-transpile the UI's `.tsx` to `.superui/build/*.js`. Build scripts run on
//! the host, so the transpiler never enters the wasm binary.
fn main() {
    supersolid::build::transpile_dir("assets/ui/counter");
}

with the matching build dependency:

[build-dependencies]
supersolid = "0.3"

This emits assets/ui/counter/.superui/build/app.js, which a plain cargo run or a wasm build loads instead of the .tsx. Keep the generated output out of version control:

**/.superui/
superui_modules/

Building for the web

A wasm build additionally needs the JS getrandom backend (the embedded JS engine pulls in randomness) and a WebGL2 renderer:

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }
bevy = { version = "0.19", features = ["webgl2"] }

To bind Bevy’s window to a canvas on the host page, set the canvas selector on the primary window when targeting wasm — see the counter example’s main.rs for the pattern.

Hot reload

superui’s hot reload isn’t a bespoke mechanism — it rides on Bevy’s standard asset hot-reloading. A UI’s index.html, .tsx, and .css are ordinary Bevy assets, so when their files change on disk, Bevy reloads them exactly like it would any other changed asset, and superui reacts by rebuilding the affected UI.

That means the watching itself is set up the usual Bevy way: through Bevy’s asset file watcher, which is enabled by the file_watcher cargo feature. If you’ve turned on asset hot-reloading in a Bevy project before, this is the same knob — and the hmr feature in Live .tsx with hot reload turns bevy/file_watcher on for you, so cargo run --features hmr is watching out of the box. (See Bevy’s hot-reloading docs for the general mechanism.)

What superui adds on top is state preservation. When the hmr feature is on and the asset server is watching, saving a .tsx (or .css) re-runs the UI on the same engine — and it does so state-preservingly. Rather than resetting everything, render() rehydrates each component’s signals (matched by module, component instance, and creation order), rebuilds the DOM fresh, and pours the old values back in. A running counter keeps its count; an open menu keeps which tab was selected.

Some edits necessarily reset state:

  • Adding or removing a signal in a component changes its signal “shape”, so that instance starts from its fresh defaults.
  • List rows preserve state by identity: <For> rows by item identity, <Index> rows by position. See Control Flow.

Hot reload is a development convenience — with the feature off, or with no file watcher, render() takes its normal path and none of this machinery runs.

PROJECTSUPERUI · BEVY UI
DRAWN BYSTROWK
SCALENOT TO SCALE
REVR1 · 0.1 EARLY BUILD
LICENCEMIT / APACHE-2.0