Interactivity with htmx
Koyo stays static-first and stays on the server for behavior. htmx 2.0 is vendored in, and pages can answer with small HTML fragments that the browser swaps in place.
htmx is built in#
The scaffold ships htmx 2.0.4 at /htmx.min.js and loads
it from the root layout:
# app/layout.py
script(src="/htmx.min.js")
Because hx_ attributes pass through the tag factory to
their hyphenated form, you write htmx directly in components:
button(
class_="btn",
hx_get="/hello",
hx_swap="outerHTML",
)("Say hello")
Fragments#
A page module can export a fragment() function. When the
request carries the HX-Request header (every htmx
request does), a fragment route returns just the fragment, without
the surrounding layout:
# app/greet/page.py
from koyoapp.html import p
def page():
# A non-htmx visitor sees the full page.
return "Full page markup for /greet."
def fragment(request):
# An htmx request gets only this snippet to swap in.
return p("Hello from a fragment.")
Koyo adds dedicated support for this pattern: a route file that only
defines fragment() always returns a bare fragment, so an
hx_post to that route never ships the full page markup
down the wire.
Working example: the session counter#
The scaffold's single home page demonstrates the full loop with the
counter in components/counter.py:
-
A button posts to the
/__koyo_stateURL thatuse_state's action fills in automatically, see session state. - Koyo increments the session value and re-renders the card with the new count.
-
hx_target="#visit-card"withhx_swap="outerHTML"swaps the response in place, replacing the old card. No page reload.
Open the home page of your app, click the plus button, and watch the count update without a navigation.
When to use it#
- Use fragments for counters, forms, search results, tabs, and inline updates.
- Prefer leaves-provided behavior over hand-written JavaScript; a fragment is still server-rendered HTML.
- Reserve a full page and heavy client logic for interactions that htmx cannot express well.