Skip to content

Quickstart

A working counter in three files: a component pair plus a one-call mount. There is no build step and no Node toolchain to install.

1. The component — two sibling files, one stem

A situ component is two files sharing a stem, so each gets native editor tooling: .py files are real Python (type-checked by your checkers), .html files are real HTML.

from situ import Local

count: Local[int] = 0  # client state — compiled to JS, no network


def bump() -> None:
    global count          # names the local signal this handler writes
    count = count + 1
<div data-region>
  <button @click="bump">+1</button>
  <strong :text="count"></strong>
</div>

count declares its site: Local means the browser owns this value. Because bump touches only local state (no await), the compiler classifies it as a client handler and compiles its body to JavaScript. The global count line is ordinary CPython — it names the signal the handler writes, which also keeps the file valid for the type checkers.

2. The mount

app.py
from pathlib import Path

import situ
from litestar import Litestar
from litestar.plugins.jinja import JinjaTemplateEngine
from litestar.static_files import create_static_files_router
from litestar.template.config import TemplateConfig
from situ import mount_static_component

HERE = Path(__file__).parent

app = Litestar(
    route_handlers=[
        mount_static_component(
            path="/counter",
            stem=HERE / "counter",       # counter.py + counter.html
            template="page.html",        # situ ships a minimal default
            meta={"name": "Counter"},
        ),
        # serve the runtime shim the generated island loads from /static/_rt.js
        create_static_files_router(path="/static", directories=[situ.static_dir()]),
    ],
    template_config=TemplateConfig(
        directory=situ.templates_dir(), engine=JinjaTemplateEngine
    ),
)

3. Run it

litestar --app app:app run
# open http://localhost:8000/counter

What just happened

The mount compiled the pair once and derived two routes:

Route Serves
GET /counter the rendered page, with a #siting-signals bootstrap ({"local": {"count": 0}, ...})
GET /counter/island.js the generated island@click became a listener, bump's body became a compiled function, :text became a subscribed DOM update

Open /counter/island.js in the browser: it is a few kilobytes of readable JavaScript with a header that says exactly what it is. The page also loads the shared shim _rt.js (signal store + wire mechanics, ~500 lines, no expression interpreter) — everything app-specific is generated.

Two rules the runtime enforces

  1. Every reactive element (anything carrying a binder or event) must live inside <header> or the single <div data-region> — those are the two roots where binders are wired at boot.
  2. data-region must be the first attribute on that <div>.

Break either and the element simply isn't bound; keep the rules and server re-renders can never orphan a listener. See Components.

Where next