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 earns its keep (the project tried a single .pyx file first): each half gets its language's native editor tooling. Python gets highlighting, navigation, and type checking; the template gets HTML tooling. 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.
  • global name on a bare-name signal write is ordinary CPython (the emitter emits nothing for it), and required: without it the name is a local of the handler, in Python and in the compiler alike, and assigning a signal's name that way is a CompileError.
  • Component .py files are type-checked by ty/pyrefly/mypy but excluded from ruff reflowing in this repo: the hand-aligned signal table is part of the authoring surface.

The .html half

The template is 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 and are 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; 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 and 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 receives two listeners, so 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.js → widgets.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.