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 transparentAnnotatedalias — to a type checkerLocal[str]isstr, 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 nameconvention on bare-name signal writes is ordinary CPython (the emitter emits nothing for it); it exists so the file is valid, checkable Python. - Component
.pyfiles 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>
- Every reactive element lives inside
<header>or the single<div data-region>. An element outside both is never bound — silently inert. data-regionmust 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 minimalpage.htmldefault (situ.templates_dir()). The page includes the#siting-signalsbootstrap, loads/static/_rt.js, then the mount'sisland.js. situ.static_dir()holds the shim; serve it at/static. Kit users also servesitu_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.