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

Components & JSX

A component is the basic building block of a superui interface. It is a plain function that returns markup. You compose components to build a UI, and you use them by writing them as tags.

function HelloWorld() {
  return <div class="greeting">Hello, world</div>;
}

// use it like a tag:
render(() => <HelloWorld />, document.getElementById("root"));

Components run once

This is the single most important thing to understand about supersolid, and it is what makes the rest of the reactivity guide make sense.

A component function runs exactly once — when it is first created. It is not re-run when state changes. Instead, the reactive expressions inside the returned markup are wired up individually, and only those re-run when their data changes.

That means the body of a component is setup code. Create your signals, derive your values, define your handlers — all of it runs a single time. Anything that needs to stay live must be a reactive expression the framework can re-evaluate on its own.

function Counter() {
  // setup: runs once
  const [count, setCount] = createSignal(0);

  return (
    // {count()} is a reactive binding — it, not the function, re-runs
    <button onClick={() => setCount(count() + 1)}>
      clicked {count()} times
    </button>
  );
}

Keep reactive reads inline

Because the body runs once, reading a signal into a plain variable at the top captures a single snapshot that never updates:

function Broken() {
  const [count] = createSignal(0);
  const now = count();          // ❌ read once, frozen forever
  return <span>{now}</span>;    //    always shows 0
}

Read the signal where the value is used, so the read happens inside a reactive binding:

function Works() {
  const [count] = createSignal(0);
  return <span>{count()}</span>; // ✅ re-runs when count changes
}

See Signals for why the location of the read is what matters.

JSX

The markup you return is JSX — an HTML-like syntax embedded in your code. superui renders it into a browser-like DOM backed by bevy_ui, so the tags are ordinary elements (div, span, button, ul, li, input, …), not a fixed component set.

Expressions

Anything in {…} is a JavaScript expression:

<span>{user().name}</span>
<div>{2 + 2}</div>

When an expression reads a signal, that spot in the DOM becomes a live binding and updates on its own.

Attributes

Attributes take a string literal or an expression. class sets the CSS class; style takes an inline style string:

<li class={todo.done ? "todo completed" : "todo"}>
  {todo.title}
</li>

<div class="bar" style={`width: ${percent()}%`}></div>

For the DOM and CSS surface superui supports, see the JS / DOM and CSS references.

Event handlers

on* attributes attach event handlers. The handler is a function:

<button onClick={() => setOpen(true)}>Open</button>
<input onInput={(e) => setDraft(e.target.value)} />
<input type="checkbox" onChange={() => toggle()} />

Common events include onClick, onInput, onChange, onKeyDown, and onKeyUp. The event object exposes target, key, and the standard propagation controls — see Events.

Props

A component receives its attributes as a single props object:

function TodoItem(props) {
  return (
    <li class={props.todo.done ? "todo completed" : "todo"}>
      <span class="label">{props.todo.title}</span>
      <button class="destroy" onClick={() => props.onRemove(props.todo.id)}>x</button>
    </li>
  );
}

// pass props as attributes:
<TodoItem todo={item} onRemove={remove} />

Props carry anything — values, objects, and callbacks (onRemove above) that let a child ask its parent to do something.

Read props where you use them. Just like signals, reactive props should be read inside the markup ({props.todo.title}), not destructured into locals at the top of the body — destructuring reads once and loses reactivity.

Composition

Components nest freely. Break a screen into small, focused components and assemble them:

function App() {
  const [todos, setTodos] = createSignal([]);
  return (
    <div id="app">
      <h1>todos</h1>
      <Header onAdd={(t) => setTodos([...todos(), t])} />
      <ul>
        <For each={todos()}>
          {(todo) => <TodoItem todo={todo} />}
        </For>
      </ul>
    </div>
  );
}

<For> and the other list/branch helpers are covered in Control Flow.

One module, no cross-file imports

superui compiles each UI into a single module, and its transpiler strips imports between your own source files. In practice: put every component for one UI in that UI’s app.tsx, as plain functions in one file. The only import you keep is the framework itself:

import { createSignal, createMemo, For, Show, render } from "supersolid";

Everything else — your components, types, helpers — just lives in the same file and refers to each other directly.

Next

  • Signals — reactive state, and why read location matters.
  • Control Flow — rendering lists and conditional branches.
PROJECTSUPERUI · BEVY UI
DRAWN BYSTROWK
SCALENOT TO SCALE
REVR1 · 0.1 EARLY BUILD
LICENCEMIT / APACHE-2.0