HTML
Stario builds HTML as Python call trees, not template files. You compose tags with normal function calls; handlers pass the tree to responses.html, which renders it and sends the response. Escaping and attribute rules live in stario.markup so views stay plain Python.
How to build a page
1. Import the tag catalog — convention is h for HTML, svg for vector tags:
from stario.markup import html as hAdd helpers as you need them (render, classes, styles, baked, …). Full package surface:
from stario.markup import ( Comment, HtmlElement, SafeString, aria, baked, classes, data, html as h, render, styles, svg,)Lighter paths when you only need part of that:
import stario.markup.html as h # tags onlyfrom stario.markup.render import render # serialize onlyImport markup from stario.markup (for example from stario.markup import html as h) — not a separate stario.html module.
2. Build a tree — each tag is a callable. Dicts before the first child become attributes; nested tags and strings are children:
def home_view(title: str) -> HtmlElement: return h.HtmlDocument( h.Head(h.Title(title)), h.Body( h.H1(title), h.P("Hello from Stario."), ), )Use h.HtmlDocument for full pages (<!doctype html> included). Use h.Html or a fragment (h.Div(...)) for partial trees and SSE patches.
3. Send it from a handler — pass the tree to responses.html; you rarely call render() yourself in route code:
import stario.responses as responsesfrom stario import Context, Writer async def home(c: Context, w: Writer) -> None: responses.html(w, home_view("Hello"))responses.html accepts a str, bytes, or HtmlElement tree. For long-lived SSE, use SSE(w).patch_elements(fragment) instead (Datastar).
4. Split views from handlers — typical layout:
| Module | Role |
|---|---|
views.py | Pure functions returning HtmlElement trees (home_view, page, …) |
handlers.py | async def routes: read c.req, call views, responses.html(w, …) |
urls.py | UrlPath constants for registration and .href() in markup |
Datastar attributes (data.on, data.signals, …) come from from stario.datastar import data, not stario.markup.data (static data-* only). See Hello world for a full example.
Annotate view parameters with HtmlElement from stario.markup when a function takes nested markup (def page(body: HtmlElement)).
Package layout
| Import | Use for |
|---|---|
stario.markup.html (h) | HTML tag catalog — h.Div, h.P, h.HtmlDocument, … |
stario.markup.svg | SVG tags — svg.Circle, svg.Path, … |
stario.markup.render | render(tree) → str (tests, emails, debugging) |
stario.markup.classes, styles, data, aria | Attrs fragments for class, style, static data-*, aria-* |
stario.markup.baked | @baked layouts (optional performance) |
stario.markup.Tag | Custom element names not in the catalog |
stario.markup.types | TagAttributes, AttributeValue for typing |
Type aliases: HtmlElement, SafeString, Attrs, Comment export from stario.markup.
For Datastar reactive attributes use from stario.datastar import data. stario.markup.data is only for static data-* HTML attributes on ordinary elements.
Tags and SafeString
A Tag is a callable factory for one element name. Built-in tags use PascalCase so call sites read like h.Div(...). Define custom elements the same way:
from stario.markup import Tag, renderfrom stario.markup import html as h UserCard = Tag("user-card")render(UserCard({"class": "rounded border p-4"}, h.P("Hello")))For void elements, pass empty="void" to Tag. Built-ins such as h.Br, h.Img, and h.Hr already do this. Custom tag names must match ^[A-Za-z][A-Za-z0-9-]*$ (e.g. user-card).
h.HtmlDocument prepends <!doctype html> before the root <html> element. h.Html renders a plain <html> tag for fragments and tests.
SafeString wraps markup inserted verbatim (no escaping). Ordinary str children are escaped for text context; attribute values go through attribute escaping.
Comments
Comment renders <!-- … -->. Text is escaped so -- and > cannot break out of the comment:
from stario.markup import Comment, SafeString, render render(Comment("note")) # <!--note-->render(Comment("a -- b")) # <!--a -- b--> (escaped)render(Comment(SafeString("x"))) # trusted bodyAccepted content: str, SafeString, int, float, None (empty comment). bool is rejected — convert explicitly if you need it as text.
Children: attribute dicts, Attrs, and content
When you call a tag, positional arguments are processed in order:
A mapping (usually a
dict): merged into the opening tag as attributes. Several dicts in a row are allowed; they are not deduplicated.An
Attrsfragment from helpers such asclasses(),styles(),data(),aria(), ordata.bind(...)fromstario.datastar.Anything else (strings, numbers, lists,
SafeString, nested tag calls): child content. Uselistfor fragments.Noneis skipped;intandfloatrender as text.Raw
boolis not valid as a child. Use booleans in attribute dicts for boolean attributes, or branch so you omit the node.
render(a, b) accepts multiple root nodes. Attributes must come before the first non-mapping, non-Attrs child.
from stario.datastar import datafrom stario.markup import classes, html as h, render view = h.Div( {"class": "card", "id": "main"}, data.signals({"open": False}), h.H2("Title"), h.P("Body copy."),)render(view) render(h.Div(classes("btn", "primary"), "Save"))Attrs
Attrs holds trusted, pre-rendered opening-tag attribute bytes (including leading spaces).
Markup helpers —
classes(),styles(),data(),aria()fromstario.markupreturnAttrs.Datastar —
data.on,data.signals, … fromstario.datastaralso returnAttrs.
Pass Attrs as tag positional arguments alongside attribute dicts, not inside dict values.
from stario.datastar import datafrom stario.markup import classes, html as h h.Button( {"type": "submit"}, data.on("click", "@post('/save')"), classes("btn", "primary"), "Save",)Attributes
Attribute dicts use string keys. Keys are validated: no whitespace, quotes, =, or other delimiter characters. Framework-style names such as @click and :class are allowed in plain dicts.
Values must be scalars: strings, numbers, SafeString, bool, or None.
Strings and numbers — quoted attributes with escaping (always double-quoted on the wire; single quotes in values stay literal).
SafeString— inserted verbatim when you control the bytes.True— boolean attribute present with no value (<details open>).False/None— attribute omitted.
For class, style, static data-*, and aria-*, use classes(), styles(), data(), and aria() from stario.markup. classes({"primary": cond}) includes tokens whose values are truthy (not strictly bool). styles() values must be str, int, or float — not SafeString. Property names cannot be @rules or contain :;{}. List or dict values inside a plain attribute dict are rejected.
For other prefix-* attributes (beyond data / aria), use from stario.markup.attributes import prefixed.
from stario.markup import aria, classes, data, html as h, stylesfrom stario.markup.attributes import prefixed h.Button({"type": "submit"}, classes("btn", "btn-primary"), "Save")h.Details({"open": True}, h.Summary("More"), h.P("…"))h.Button(data({"user-id": "42"}), "Go")h.Div(prefixed("hx", {"post": "/save", "target": "#main"}))Types
HtmlElement is the union type for trees, children, and render() roots — use it on view signatures (def page(body: HtmlElement)). TagAttributes and AttributeValue in stario.markup.types annotate attribute dicts.
SafeString and escaping
Text nodes:
escape_textescapes&,<,>.Attribute values:
escape_attribute_valuealso escapes". Values are always emitted in double quotes.SafeStringis emitted as-is in both contexts.
If markup comes from users, escape or sanitize first. Stario does not second-guess SafeString.
render() raises if you pass an uncalled Tag (e.g. render(h.Div) instead of render(h.Div())).
Baking (@baked)
Most pages never need @baked. Use it when a stable layout is invoked often and you have measured allocation or render cost.
Without @baked, each call allocates a fresh element tree and render walks it. With @baked, the decorator captures the layout once at import into a segment plan; each call splices dynamic parameters and returns a SafeString fragment.
Dynamic values must appear as element children or whole attribute values. They cannot appear inside styles(), classes(), or data(), or as attribute names. Build Attrs or style strings outside the builder and pass them in:
from stario.markup import SafeString, baked, classes, renderfrom stario.markup import html as h @bakeddef row(label, class_attrs): return h.Li(class_attrs, label) @bakeddef badge(label, style_value): return h.Span({"style": style_value}, label) render(row("Save", classes("btn", "primary")))render(badge("New", SafeString("color:red;")))Rejected at decoration time (when @baked runs, not on first call):
Unused parameters — every parameter must appear as a child or whole attribute value.
Boolean literals and uncalled
Tagobjects in the builder tree.Parameters used as attribute names (only values).
Parameter names starting with
__stario.
Other rules: signatures cannot use *args, **kwargs, or positional-only /. Keyword-only parameters and defaults are supported (inner=None omits the child when not passed). Nested calls to other baked functions return SafeString fragments that compose without double-escaping. A no-parameter builder is frozen once at import into a fixed SafeString.
Comparisons on parameters are not supported inside the builder (==, truthiness, if / and / or). Identity checks (is, is not) are not guarded either — param is not None is always true at bake time, so branch outside the builder.
from stario.markup import baked, renderfrom stario.markup import html as h @bakeddef shell_fast(title, body): return h.Div(h.Title(title), body) render(shell_fast("Docs", h.P("Hello.")))SVG
Vector tags live in stario.markup.svg. SVG uses camelCase attribute names (viewBox, stdDeviation) and literal colon keys ("xlink:href"). For standalone SVG documents, set xmlns on the root svg.Svg element.
Empty modes matter when mixing HTML and SVG:
| Mode | Behavior | Examples |
|---|---|---|
void | Self-close; children are an error | h.Br, h.Img, Tag("br", empty="void") |
self_closing_when_empty | Self-close when empty; allow children when present | svg.Circle, svg.Path |
normal (default) | <tag></tag> when empty | h.Div, svg.G |
from stario.markup import Tag, svg svg.Svg({"viewBox": "0 0 100 100"}, svg.Circle({"cx": "50", "cy": "50", "r": "40"}))CustomDot = Tag("circle", empty="self_closing_when_empty")API
class Tag(name, *, empty='normal', prefix='')
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.markup.html and stario.markup.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).
empty="normal" — no children renders <tag></tag>; children allowed. empty="void" — no children renders <tag/>; children are an error (HTML br, img, input, …). empty="self_closing_when_empty" — no children renders <tag/>; children allowed when present (SVG leaves like circle, path, filter primitives).
Calling the instance (h.Div(...), h.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 (
dictor otherMapping) is rendered as flatkey=valueattributes in the order it appears. Several mappings in a row are allowed, but they are not merged or deduplicated; duplicate attributes render as duplicate attributes.Noneentries are skipped.The first non-mapping starts child content. After that, mappings must not appear (attributes must come first).
Return value: a (open, attrs, children, tail) four-tuple, or a cached SafeString when there are no arguments. With no attributes, open is the tag start including > (tag_start_no_attrs); otherwise open is "<name" and attrs is a joined string or slot list. children is None for empty elements; tail is no_children_close or closing_tag.
Tag.__call__(*children)
Build one element from positional arguments.
Children are processed in order:
Each initial mapping (
dictor otherMapping) is rendered as flatkey=valueattributes in the order it appears. Several mappings in a row are allowed, but they are not merged or deduplicated; duplicate attributes render as duplicate attributes.Noneentries are skipped.The first non-mapping starts child content. After that, mappings must not appear (attributes must come first).
Return value: a (open, attrs, children, tail) four-tuple, or a cached SafeString when there are no arguments. With no attributes, open is the tag start including > (tag_start_no_attrs); otherwise open is "<name" and attrs is a joined string or slot list. children is None for empty elements; tail is no_children_close or closing_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 SafeString values. Pass them as one root, e.g. render(layout(a, b)). You can also nest layout(...) wherever an element child is accepted, same as h.Div(...).
baked(fn=None)
Compile fn once into a segment plan, then return a fast callable.
Parameters may appear as element children or whole attribute values. Every call returns a SafeString with dynamic children and attributes already rendered.
Use this for repeated application views whose structure is stable across calls. Keep baked builders declarative: compute conditionals, loops, and derived strings outside the builder, then pass the finished child fragment or whole attribute value in as a parameter.
Invalid static markup (boolean literals, uncalled tags, unsupported types) and unused parameters are rejected when the decorator runs, not on the first render call.
Equality and truthiness on parameters (==, if / and / or) are not supported inside the builder — placeholders stand in at decoration time. Identity checks (is, is not) are not guarded: param is not None is always true while the builder runs.
Comment(content='')
<!-- ... --> with textual content escaped so --> cannot break out.
Parsers do not decode entities inside comments, so escaped characters (e.g. >) read back literally: escaping protects against breakout, not byte-for-byte fidelity. Pass SafeString when you control the bytes and need them verbatim.
classes(*tokens)
Return a pre-rendered class attribute fragment.
Mapping values are truthy-tested (not strictly bool): any truthy value includes the class name.
styles(declarations)
Return a pre-rendered style attribute fragment.
data(attrs)
Return pre-rendered data-* attribute fragments from a flat mapping.
aria(attrs)
Return pre-rendered aria-* attribute fragments from a flat mapping.
prefixed(prefix, attrs)
Return pre-rendered prefix-name attribute fragments from a flat mapping.
Internal primitive for data() and aria(). For other prefixes import from this submodule, e.g. from stario.markup.attributes import prefixed.
class SafeString(rendered)
Markup inserted verbatim into rendered HTML (no escaping).
Fields
- rendered(str):—
class Attrs(rendered)
Trusted/pre-rendered opening-tag attributes, including leading spaces.
Fields
- rendered(str):—