Components
Caspian components are Python functions decorated with
@component.
Keep readable HTML markup inside html(...),
import the component where it is used, and render its HTML-first
x-* tag.
1. The Basics
A component merges incoming attributes, forwards browser-facing props,
and returns its markup through the single-file html(...)
helper. Use merge_classes so
caller classes compose safely with component defaults.
from casp.component_decorator import component, html from casp.html_attrs import get_attributes, merge_classes @component def Container(children="", **props): incoming_class = props.pop("class", "") attributes = get_attributes( "class": merge_classes("mx-auto max-w-7xl px-4", incoming_class), , props) return html(r""" <div {{ attributes }}> {{ children | safe }} </div> """, attributes=attributes, children=children)
2. Typed Props And Forwarding
Named Python parameters document the server API. Any value referenced by
pp.props or a brace expression must
also be emitted on the component's single native root with
get_attributes(...).
Python consumes named arguments, merges classes, and deliberately forwards every prop the browser template reads.
Brace expressions preserve their runtime type. Literal attributes arrive as strings, while omitted values read as undefined.
3. Inline Markup And Context
Keep server interpolation and a small owned PulsePoint script together in the component's Python file. Double braces are Jinja values; single braces remain browser-side PulsePoint expressions.
@component def Counter(label="Clicks"): return html(r""" <div> <h3>{{ label }}</h3> <button onclick="setCount(count + 1)"> {count} </button> <script> const [count, setCount] = pp.state(0); </script> </div> """, label=label)
4. Async Components
Use an async component only when its render contract must await I/O. Keep page-level fetching in the route and pass the result down when the data belongs to the page workflow rather than the reusable component.
Mental Model
Use a normal function for immediate markup. Use an async function when the component itself needs a database, service, or file result before it can render.
@component async def ProfileCard(user_id: str): user = await get_user_by_id(user_id) return html(r""" <article class="rounded-xl border p-4"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> </article> """, user=user)
5. Server Actions With RPC
A component can own interactive browser state while the route owns the
Python @rpc() action. Call it
with pp.rpc(...), then update
only the state that renders.
6. Component Composition
Import child components with normal Python imports inside the module that
authors their x-* tags. Split
substantial page sections by responsibility while keeping the route page
itself a short assembly.
Route shell
Imports and assembles page sections.
Feature section
Owns a form, table, toolbar, or panel.
Reusable leaf
Owns repeated markup and small interactions.
7. Fragments (Multiple Roots)
A component whose template has sibling top-level nodes is a fragment —
the same shape React writes as
<>…</>. The syntax
is implicit: write the siblings, and nothing else. Caspian frames them with a
boundary the browser materializes at mount, so a fragment adds
no element to the rendered tree.
@component def TableRows(rows): return html(r""" {% for row in rows %} <tr><td>{{ row.name }}</td></tr> {% endfor %} """, rows=rows)
A fragment cannot receive props
Props arrive as attributes on a rendered root element, and a fragment has
none, so pp.props would be silently empty.
Passing any attribute — including
pp-ref — on the
x-* tag of a fragment component raises
FragmentPropsError at render time. Give the
component a single native root when it needs props.
index.py or
layout.py is wrapped in a layout-neutral
display: contents host rather than a comment
boundary, so sibling page sections need no meaningless wrapper
<div>. Never handwrite the boundary
markers or <pp-fragment> yourself —
they are compiler output.