HTML

The stario.html package is Stario’s way of building a document tree with normal Python syntax. You compose values; render turns them into a string. That keeps structure and escaping rules in one place, so you get more control and fewer surprises than hand-maintaining HTML strings or feeding opaque text through a template engine like Jinja.

Prefer importing from the stario package root and aliasing HTML once:

python
from stario import html as h

Then use h.Div, h.render, and so on. SVG uses the same idea at the root: from stario import svg, then svg.Svg, svg.Circle, etc. (That is the same module as stario.html.svg.) Importing every HTML tag (from stario.html import Div, P, Span, …) is fine if that fits your style; the html as h pattern stays readable as pages grow.

Tags and SafeString

Two ideas sit at the center of the API.

A Tag is a small factory for one HTML element name. In tags.py, Stario defines P = Tag("p"), Div = Tag("div"), and so on. By convention those bindings start with an uppercase letter (PascalCase) so call sites read like Div(...) and P(...) instead of clashing with the builtin div. Under the hood they are still Tag instances—callable objects.

You can define your own, including custom elements:

python
from stario.html import Tag, render
 
P = Tag("p")  # same idea as the built-in helpers
UserCard = Tag("user-card")
 
render(UserCard({"class": "rounded border p-4"}, P("Hello")))
# <user-card class="rounded border p-4"><p>Hello</p></user-card>

For void elements, pass self_closing=True (for example Tag("img", self_closing=True)). Built-ins such as Br, Img, and Hr already do this so a call like Img({"src": "x.png"}) becomes <img src="x.png"/> with no separate closing tag.

SafeString wraps text that must go to the wire without extra escaping. The renderer trusts it completely. Use it only when the bytes are yours (or you accept the risk). Ordinary str children are escaped for text context; attribute values go through attribute escaping (quotes, &, and so on). See SafeString and escaping below.

Children: mappings vs everything else

When you call a tag, you pass positional arguments in order. Each argument is either:

  • A mapping (usually a dict): merged into the opening tag as attributes. Several dicts in a row are merged in order (later keys win). This is why Datastar helpers work cleanly: they return small dicts you drop next to your own attribute dicts.

  • Anything else (strings, numbers, lists of nodes, SafeString, nested tag calls): becomes child content after attributes. Use list for fragments; the only tuple form the renderer treats as a special node shape is the internal three-element element tuple from tags—not arbitrary tuples of children.

  • Not valid as a child: raw bool (True / False). The renderer rejects booleans in child position—use True / False in attribute dicts for boolean attributes, or wrap text with str(...), or branch so you omit the node when off.

Attributes must come before the first non-mapping child; the implementation enforces that so the tree is unambiguous.

python
from stario import html as h
 
view = h.Div(
    {"class": "card", "id": "main"},
    {"data-expanded": "false"},
    h.H2("Title"),
    h.P("Body copy."),
)
h.render(view)
# <div class="card" id="main" data-expanded="false"><h2>Title</h2><p>Body copy.</p></div>

Attributes

Attribute dicts use string keys (or SafeString keys). Values can be:

  • Strings and numbers — written as quoted attributes with appropriate escaping.

  • SafeString — inserted verbatim into the attribute value when you control the bytes (same trust rules as body text).

  • True — boolean attribute present with no value (<details open>).

  • False — attribute omitted (handy for toggling hidden, disabled, etc.).

  • None — boolean-style presence: the attribute name is emitted with no value (same wire shape as True). Tests rely on this for flags such as required.

  • Lists — joined with spaces; typical for class: ["btn", "primary"]class="btn primary".

  • Nested dicts under special keys — see Nested data, aria, and style.

python
from stario import html as h
 
h.Button({"type": "submit", "class": ["btn", "btn-primary"]}, "Save")
# <button type="submit" class="btn btn-primary">Save</button>
 
h.Details({"open": True}, h.Summary("More"), h.P("…"))
# <details open><summary>More</summary><p>…</p></details>
 
h.Div({"hidden": False, "id": "x"}, "visible")
# <div id="x">visible</div>

Nested data, aria, and style

For data, aria, and style, the value may be a nested dict. Stario flattens it when rendering:

  • data: { "star": "1" }data-star="1" (keys are escaped and prefixed with data-; use wire-ready key strings such as "star" or "user-id"—there is no automatic camelCase→kebab rewrite).

  • aria: { "label": "Close" }aria-label="Close".

  • style: { "color": "red", "margin-top": "8px" } → a single style attribute with a safe CSS string (color:red;margin-top:8px).

python
from stario import html as h
 
h.Button({"aria": {"label": "Close"}}, "×")
# <button aria-label="Close">×</button>
 
h.Div({"style": {"color": "red", "font-weight": "bold"}}, "Hi")
# <div style="color:red;font-weight:bold;">Hi</div>

Other attribute namespaces can use nested dicts too; the same flattening rules apply, including True / False / None for boolean-style keys (see render_nested / render_styles in the source for edge cases).

SafeString and escaping

  • Text nodes (plain str in child position): escape_text turns &, <, > into entities; quotes stay as-is.

  • Attribute values use escape_attribute_value — it also escapes " and ' so values are safe inside double-quoted attributes.

  • SafeString is emitted as-is in both contexts (no escaping).

If you need raw HTML from your own literals, wrap it in SafeString deliberately. If it comes from users, escape or sanitize first—Stario will not second-guess you.

Baking (@baked)

Most pages and components never need @baked: ordinary tag functions are simpler and stay the default. Reach for @baked only when you have a hot path with a stable layout and you have measured allocation or render cost.

@baked is a performance tool, not a mini template language.

Without @baked, every call to your builder runs tag factories again: new (open, children, close) tuples (and nested trees) are allocated, and a full render walk turns that tree into a string each time.

With @baked, work is split into two phases. When the decorator runs (typically at import), Stario calls your function once with internal placeholders for each parameter. That tree is flattened into an ordered sequence of segments: runs of static markup are merged into longer SafeString pieces, and each dynamic parameter becomes a slot at fixed indices. On each real call, the wrapper splices the arguments you pass into those slots—same order, same structure—without rebuilding the static spine. You get a fragment (list of nodes, or a single SafeString when there are no parameters). Pass it to render as a single root—render flattens lists, so you do not need to star-unpack. Use the fragment like any other element when nesting (see below).

Dynamic values must appear as element children, not inside attribute dicts. Signatures cannot use *args / **kwargs or positional-only /; normal and keyword-only parameters (after *) are fine.

A zero-argument @baked function has nothing to splice: the whole layout is precomputed into one SafeString, so each call just returns that cached value (still useful to lock markup at import time).

python
from stario import html as h
 
 
@h.baked
def shell_fast(title, body):
    return h.Div(h.Title(title), body)
 
 
# One string for the whole fragment—pass the list as a single render root (no *).
h.render(shell_fast("Docs", h.P("Hello.")))
 
# Or use shell_fast like any other tag call—nest it wherever a child node goes.
h.Div({"class": "page"}, shell_fast("Docs", h.P("Hello.")))

Use ordinary functions when you need maximum flexibility (*args inside helpers, dynamic attribute dicts, and so on). Use @baked when the layout is stable and invoked often.

SVG

Vector tags live in stario.html.svg, also available as from stario import svg. Python bindings use the same PascalCase convention as HTML (svg.Circle, svg.LinearGradient, …). Use the same attribute/child rules as HTML:

python
from stario import svg
 
svg.Svg({"viewBox": "0 0 100 100"}, svg.Circle({"cx": "50", "cy": "50", "r": "40"}))

API

class Tag(name, self_closing=False)

Callable factory for a single HTML or SVG element name.

Instantiate with the constructor (e.g. P = Tag("p")). Prefer the built-in catalogs in stario.html.tags and stario.html.svg where they exist. The factory caches opening/closing strings and, for tags with no arguments, a ready SafeString (avoid mutating these attributes after init).

Calling the instance (Div(...), P("hi"), etc.) builds a tree node; see Tag.__call__.

Tag.__call__(*children)

Build one element from positional arguments.

Children are processed in order:

  • Each initial mapping (dict or other Mapping) is merged into the opening tag as attributes; several mappings in a row merge left-to-right (later keys win). None entries are skipped.

  • The first non-mapping starts child content. After that, mappings must not appear (attributes must come first).

Return value: a (open_tag, children, close_tag) tuple for elements with children, or a SafeString for empty or self-closing results. With no arguments, returns the cached empty / self-closing markup for this tag.

Tag.__call__(*children)

Build one element from positional arguments.

Children are processed in order:

  • Each initial mapping (dict or other Mapping) is merged into the opening tag as attributes; several mappings in a row merge left-to-right (later keys win). None entries are skipped.

  • The first non-mapping starts child content. After that, mappings must not appear (attributes must come first).

Return value: a (open_tag, children, close_tag) tuple for elements with children, or a SafeString for empty or self-closing results. With no arguments, returns the cached empty / self-closing markup for this tag.

render(*nodes)

Walk HTML fragments depth-first and return one UTF-8 string.

Accepts any number of root nodes (variadic). Plain str in child position is HTML text-escaped; attribute escaping happens inside Tag when the opening tag is built. SafeString is emitted unchanged (trusted).

Fragments from baked are a list of nodes (or a single SafeString when there are no parameters). Pass them as one root, e.g. render(layout(a, b)): lists are flattened while walking the tree. You can also nest layout(...) wherever an element child is accepted, same as Div(...).

baked(fn=None)

Compile fn once into a segment plan, then return a fast callable.

On decoration, fn runs with placeholder arguments so static markup is flattened to SafeString runs and each parameter gets reserved indices in the plan. Each invocation copies that plan and writes real argument nodes into those slots. With no parameters, the plan is a single precomputed SafeString returned on every call.

Dynamic parameter values must appear as element children (not inside attribute mappings). See the module docstring for constraints and examples.