Skip to content

Cheat sheet

The whole authoring surface on one page. Each section links to the full treatment.

The model, in 30 seconds

A component = name.py (sited signals + handlers) beside name.html (real HTML + :/@ binders). One mount call compiles the pair once and derives every route; the browser gets a 5–12 KB generated island over a shared shim. The site decides the transport. await in a handler makes it a server command. Everything outside the dialect is a CompileError.

Sites

Declaration Lives Client Server Set by
x: Local[T] = default browser S.get/S.set, compiled code
x: Url[str] = "all" query string read-only context(view) <a href="../?x=…">
x: Server[Facade] server any access = CompileError facade in handlers, rows in template commands
x: Synced[T] reserved

Sites & the seam

The .py side

from __future__ import annotations          # required
from situ import Local, Url, Server        # + Synced, Prop, Emit, Provide, Inject

search: Local[str] = ""
tags: Local[set] = set()                    # Local[set] is first-class
filter: Url[str] = "all"
issues: Server[IssuesFacade]                # Protocol under TYPE_CHECKING

def clear():                                # no await → compiled to client JS
    global search                           # bare-name signal write: declare global
    search = ""

async def close(id):                        # await → POST /cmd/close/{id}
    await issues.set_status(id, "closed")
    reset("search")                         # injected: back to defaults, rides the response
    signal("toast", "Closed")               # injected: push a value into a client signal

The .html side — two rules

Every reactive element lives in <header> (survives command swaps — search boxes, dialogs, toasts) or in the single <div data-region> (re-rendered by commands). data-region is that div's first attribute.

Events & binders

Token Does Notes
@click @dblclick @submit @input @escape listeners @submit preventDefaults; rows allow only @click/@submit
:text :show :class="c: expr" :attr="a: expr" DOM updates :show sets hidden — keep [hidden]{display:none!important} in your CSS
:bind="sig" two-way input Local only; type=number coerces
:each="r in list" keyed client reconciler Local[list]; on a wrapper element; rows keyed by id
:tree="r.children" self-recursive rows inside a :each
:virtual windowed rows via /window server handler + window_template=
:autohide="sig" self-clearing status
:tabs :table :combobox :datagrid+sel :menu :palette :sortable :cells kit widget runtimes load widgets.js; data stays in Locals

Server rows: {% for t in issues %} + data-id="{{ t.id }}" data-title="{{ t.title }}" → binders on/inside the row may read t.id, t.title (dataset). Full table

The dialect (client-compiled Python)

Expressions: names · literals · and or not · == != < <= > >= (numeric-normalized, strict) · + - * / % · x if c else y · in/not in (Set.has / .includes) · f-strings (no !r/format specs) · [i]/slices · {...}/[...] literals · len sum(gen) min max abs round today() · .strip() .lower() .upper() .startswith() .endswith()

Statements: x = expr (single target) · += -= *= /= %= · if/elif/else · return · s.add/.discard, l.append/.remove · reset(...) · global

Rejected, always loudly: loops · while with try yield lambda · comprehensions (beyond the sum generator) · imports · nested defs · unknown names/calls — and every client touch of Server state. Dialect · Errors

Actions (@click="…"): ;-separated sig = expr + calls. Leading local assigns before a server call = optimistic apply, auto-rollback on failure.

Mounts

# Litestar (the reference adapter; from `situ`) — lazily imported, no Litestar until called
mount_static_component(path=, stem=, template=, meta=)              # pure client
mount_component(path=, stem=, template=, meta=,                     # + the server seam
                service_type=Store, context=ctx,                    # Litestar mount: Dishka container
                components=Context.from_dir(DIR),                   # optional tree
                live=True, window_template=..., unified=True)       # opt-ins
mount_tree(path=, root=, components=, template=, meta=)             # pure-client tree

# Flask (from `situ.mount.flask`, in situ[flask]) — a Blueprint, no Dishka, no Litestar
mount_flask(path=, app_factory=lambda cmd: compile_app(front, cmd), template=, meta=,
            resolve=lambda: store, context=ctx, facade=Store)

Serve situ.static_dir() at /static; template dirs: [yours, situ.templates_dir()]. API

The command wire

POST {mount}/cmd/{name}[/{id:int}]   X-Siting: 1 · form fields + local `with`s + url params
  ← 200 region HTML                  + X-Siting-Signals: {"sig": value}   (resets/pushes)

In flight: control disabled, region aria-busy. Non-2xx: rollback, no swap. Wire protocol

Composition

ctx = situ_ui.kit().merge(Context.from_dir(COMPONENTS)).with_override("Toast", stem)
<Confirm :open="flag" @confirmed="save"/>                    <!-- Prop / Emit, name-matched -->
<Card><template slot="title">Hi</template>body</Card>        <!-- slots: parent scope -->

Provide[T]/Inject[T] = ancestor value channel. A child owning Locals + handlers is uncontrolled — place it N times, get N independent states. Child <style> scopes to the child. Cycles error; data recursion = :each + :tree. Check trees statically: python -m situ.check ROOT --from-dir DIR [--kit]. Composition

declui, in one breath

class Issue:  # @dataclass / attrs / msgspec / pydantic / SQLAlchemy (info={"declui": Field(...)})
    id: Annotated[int, Field(hidden=True)]
    title: Annotated[str, Field(label_field=True, search=True)]
    status: Status                                    # Enum → <select>
    owner: User | None                                # reference → <select>/Combobox
    notes: list[Note]                                 # one-to-many, read-only
    bio: Annotated[str, Field(widget="RichText", editable="rating > 50")]

    @action(visible="status == 'open'", message="Closed")   # async → per-row command
    async def close(self): ...

mount_model(path=, screen=Screen(model=Issue, zones={...}),          # form (default)
            screens=("list","detail"), rows=[...], choices={...})    # client modes
mount_model(path=, screen=, service_type=, facade=, context=)        # server tracker

Predicates (visible/editable/valid, object_editable) read value / siblings / editing → client binders; @action(visible=) → a server row gate. valid="cond or 'message'". declui

Gotchas

  • Components compile once per process — run with --reload during development.
  • [hidden] { display: none !important; } in your CSS, or :show loses to your display: rules.
  • Buttons in forms default to type=submit — give Cancel type="button".
  • A @submit handler's params bind from the form's name= fields.
  • The Litestar mount_component needs a Dishka container (request.state.dishka_container); the Flask mount_flask takes a resolve callable instead.
  • Local[set] crosses to commands as a JSON array.
  • A Url signal is set by navigation only; changing it resets in-progress Local state.

Machine-readable versions of this reference: /llms.txt (index) and /llms-full.txt (the single-file reference for AI coding tools).