Skip to content

Using situ

situ compiles a Python component into a JavaScript island, so interactive state can be written once, in Python, and sited explicitly — Local (client only), Url (shareable, server re-renders), Server (a command), Synced.

This system works with it in two ways.

Styling situ_ui widgets

If you use situ_ui, load the bridge after ui.css:

<link rel="stylesheet" href="/ui/ui.css">
<link rel="stylesheet" href="/static/css/ads/situ.css">

situ.css retargets situ_ui's --ui-* tokens at ours, so both stacks share one palette without reimplementing situ_ui's 168 class bodies. Forty lines of aliases rather than ~600 lines of duplication.

It deliberately does not map metrics. situ_ui is dense enterprise on purpose — 13px base type, 32px controls — and ours is 14px. That is a product decision rather than a vocabulary mismatch, so the bridge leaves it alone. The opt-in block at the end of the file adopts our metrics too, if you want them.

Writing islands with our classes

The reference application has six. The important constraint:

Macros cannot be used inside a situ component

situ compiles a component's .html at source level, so a binder that a Jinja macro emits is never seen by the compiler. Macros render server-side chrome; islands are hand-written against the class vocabulary.

<div class="ads-card">
  <div class="ads-card__body ads-stack">
    <input class="ads-input" :bind="query" placeholder="Search…">

    <ul class="ads-list">
      {% for row in rows %}
      <li :show="query == '' or query in t.name">
        <a class="ads-list__link" :attr="aria-current: 'true' if open_id == t.id else False"
           @click="open_id = t.id">{{ row.name }}</a>
      </li>
      {% endfor %}
    </ul>
  </div>
</div>

False, not '', to remove an attribute

:attr removes an attribute on False or None. An empty string is how True renders — a present boolean attribute — so else '' produces aria-current="", which is present and matches [aria-current]. Three wizard steps once lit up at once for exactly this reason.

Region swaps and vendor libraries

A Url-sited change re-renders the region, which destroys and recreates any DOM a vendor library owns. Rebuild on the swap:

window.__siting.afterSwap(draw);

The global is window.__siting, and afterSwap is the page-level hook — onSwap belongs to the island and is a different thing.

Keyboard traps in vendor editors

A rich-text field is the commonest way an application fails WCAG 2.1.2. Quill binds Tab to inserting a tab character, so focus goes in and never comes out:

keyboard: { bindings: {
  tab:         { key: "Tab", handler: () => true },
  "shift-tab": { key: "Tab", shiftKey: true, handler: () => true },
} }

Returning true means I did not handle this, so the browser's own Tab runs. Also load content with quill.setContents(…, "silent") — without it the change event steals focus on page load.