Skip to content

Components

Two files, one stem

A component is a pair of sibling files sharing a stem — issues.py beside issues.html:

components/
  issues.py      ← sited signal declarations + handler functions
  issues.html    ← real HTML: Jinja for server state, :/@ shorthand for client behavior

The split is deliberate (the project tried a single .pyx file first): each half gets its language's native editor tooling — highlighting, navigation, and type checking for the Python; HTML tooling for the template. The compiler loads the pair (load_front_end(stem)) and treats them as one source.

The .py half

from __future__ import annotations          # required — markers are never evaluated at runtime

from situ import Local, Server, Url

search: Local[str] = ""                     # a signal: name, site, default
issues: Server[IssuesFacade]                # a server resource behind a typed facade

def clear() -> None:                        # a handler: no await → compiled to client JS
    global search                           # names the signal this handler writes
    search = ""
  • Signals are module-level annotated assignments. The marker (Local/Url/Server/Synced) is a transparent Annotated alias — to a type checker Local[str] is str, so handler bodies type-check against real types; the compiler reads the site from the annotation's outer name.
  • Handlers are module-level functions. Whether one runs on the client or the server is derived from the handler's own body — see Handlers & commands.
  • The global name convention on bare-name signal writes is ordinary CPython (the emitter emits nothing for it); it exists so the file is valid, checkable Python.
  • Component .py files are type-checked by ty/pyrefly/mypy but deliberately excluded from ruff reflowing in this repo — the hand-aligned signal table is part of the authoring surface.

The .html half

Real HTML with two vocabularies side by side:

  • Jinja ({% for %}, {{ x }}) renders server state — once per render, on the server.
  • Binders and events (:text, :bind, :show, :each, @click, …) bind client state — compiled into the island. Full table: Binders & events.

The unified idiom is an opt-in experiment that collapses the two into one interpolation, deriving server-vs-live from each signal's site.

The two runtime rules

The island wires binders at boot on exactly two roots, and server commands re-render exactly one of them:

<body>
  <header>            ← bound once at boot; SURVIVES region swaps
    search box, dialogs, toasts — anything that must outlive a re-render
  </header>

  <div data-region>   ← bound at boot AND re-bound after every command swap
    everything a server command may re-render
  </div>
</body>
  1. Every reactive element lives inside <header> or the single <div data-region>. An element outside both is never bound — silently inert.
  2. data-region must be the first attribute on that <div>. The region splitter and the boot targeting depend on it.

Don't nest a <header> inside the region

Both roots get bound at boot; an element inside both would receive two listeners, and a toggle handler (x = not x) cancels itself.

Pages, templates, and static files

  • A mount renders your page template (Jinja — from Litestar's TemplateConfig, or situ's own env in the Flask adapter); situ ships a minimal page.html default (situ.templates_dir()). The page includes the #siting-signals bootstrap, loads /static/_rt.js, then the mount's island.js.
  • situ.static_dir() holds the shim; serve it at /static. Kit users also serve situ_ui.STATIC (load order: _rt.jswidgets.js → island).
  • meta={"name": ...} is passed through to your page template — the demos use it for the header chrome.

Compiled once, inspectable always

A mount compiles its component pair once (cached); editing a component means restarting the app (a watch mode is on the roadmap). The result is an EmittedApp whose parts you can see: every demo page renders its own inspector — the source pair, the signal→transport table, each handler's classification and route, and the served island. Reading the generated code is the intended way to trust it.