Build Interactive Web Apps with FastHTML (2026)
Build Interactive Web Apps with FastHTML (2026) shows how Answer.AI's FastHTML lets you ship interactive pages in pure Python. You write HTML components as Python functions, wire HTMX attributes as kwargs, and return HTML fragments instead of JSON. No React, no Vite, no separate frontend repo — one file can be a working UI.
If you already use FastAPI + HTMX for no-build UIs or FastAPI app.frontend() to serve an SPA, FastHTML is the next step when you want the HTML itself to be Python. Pair installs with uv + Ruff and you stay in one toolchain from laptop to deploy.
TL;DR
- FastHTML (
python-fasthtml) is Answer.AI's HTMX-first web stack: Python in, HTML out. - Install with
uv pip install python-fasthtml(orpip); run withserve()/ uvicorn. - Routes return components like
Div,Button,H2— not templates full of braces. - HTMX kwargs (
hx_post,hx_target,hx_swap) become realhx-*attributes. - With the
HX-Request: trueheader, partial routes return bare HTML fragments for swaps.
Why FastHTML in 2026?
Most Python web tutorials still split the world: FastAPI for JSON APIs, then a JS SPA for the UI. HTMX already flipped that for many teams — the server returns HTML. FastHTML goes further: the HTML tree is native Python, so your editor, types, and tests apply to the UI the same way they apply to the API. For dashboards, admin tools, and internal apps, that is often enough.
| Stack | You write | Best when |
|---|---|---|
| FastAPI + JSON + SPA | Python API + JS/TS UI | Heavy client state, offline, design systems |
| FastAPI + Jinja + HTMX | Python + HTML templates | You like templates and partials |
| FastHTML | Python components + HTMX | UI and server logic in one language |
Versions tested (2026-09-27)
- Python
3.14.7(venv via uv; system also has 3.13.5) python-fasthtml0.14.13- starlette
1.7.0, uvicorn0.54.0, httpx0.28.1 - HTMX loaded by FastHTML from CDN:
htmx.org@2.0.7 - Demo server:
http://127.0.0.1:5001
uv venv .venv && source .venv/bin/activate
uv pip install python-fasthtml httpx
# or: pip install python-fasthtml httpx
python main.py
# Link: http://localhost:5001
1. Minimal interactive app
Save this as main.py. The home page renders a counter and two buttons. Clicking Increment POSTs to /inc; the handler returns only the updated <h2 id="count">, and HTMX swaps it into the page.
from fasthtml.common import *
app, rt = fast_app(pico=True)
COUNT = {"n": 0}
@rt("/")
def get():
return Titled(
"FastHTML Counter",
P("A pure-Python interactive page — no React, no build step."),
Div(
H2(f"Count: {COUNT['n']}", id="count"),
Button(
"Increment",
hx_post="/inc",
hx_target="#count",
hx_swap="outerHTML",
),
Button(
"Reset",
hx_post="/reset",
hx_target="#count",
hx_swap="outerHTML",
style="margin-left:0.5rem",
),
),
P(A("About this demo", href="/about")),
)
@rt("/inc")
def post():
COUNT["n"] += 1
return H2(f"Count: {COUNT['n']}", id="count")
@rt("/reset")
def post():
COUNT["n"] = 0
return H2(f"Count: {COUNT['n']}", id="count")
@rt("/about")
def get():
return Titled(
"About",
P("Built with python-fasthtml — HTML components and HTMX in one Python file."),
A("← Back", href="/"),
)
if __name__ == "__main__":
serve(port=5001)
2. Real HTML from the running app
With the server up, a normal GET returns a full document (title, Pico CSS, HTMX script, and the counter). Notice the hx-post / hx-target / hx-swap attributes on the buttons — FastHTML mapped the Python kwargs 1:1.
curl -s http://127.0.0.1:5001/ | rg 'Count:|hx-post|htmx'
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script>
<h2 id="count">Count: 0</h2>
<button hx-post="/inc" hx-swap="outerHTML" hx-target="#count">Increment</button>
<button hx-post="/reset" hx-swap="outerHTML" hx-target="#count" ...>Reset</button>
3. HTMX partials — the interactive part
Browsers sending HTMX requests include HX-Request: true. FastHTML then returns the bare fragment your swap expects (not a full HTML page). We reproduced that with httpx:
curl -s -X POST -H 'HX-Request: true' http://127.0.0.1:5001/inc
# <h2 id="count">Count: 1</h2>
curl -s -X POST -H 'HX-Request: true' http://127.0.0.1:5001/inc
# <h2 id="count">Count: 2</h2>
curl -s -X POST -H 'HX-Request: true' http://127.0.0.1:5001/reset
# <h2 id="count">Count: 0</h2>
Same calls via httpx in the demo folder produced identical bodies — status 200 each time. That is the whole interaction model: Python mutates state, returns HTML, HTMX patches the DOM.
4. When to use FastHTML vs FastAPI + HTMX
- Prefer FastHTML when the UI is the product of Python components, you want one process and one language, and you are happy with server-driven HTML.
- Prefer FastAPI + HTMX when you already have a FastAPI JSON API, OpenAPI clients, or Jinja templates you do not want to rewrite — see our FastAPI HTMX guide.
- Prefer an SPA behind FastAPI when you need rich client routing or a separate frontend team — see serving an SPA with
app.frontend().
FastHTML still runs on Starlette/uvicorn under the hood, so deployment looks familiar. The difference is the default response type: HTML components, not Pydantic models.
Common pitfalls
- Expecting JSON. Handlers should return components or HTML strings. If you need a JSON API, expose a separate route or stick with FastAPI for that surface.
- Forgetting
hx_target/hx_swap. Without them HTMX may replace the wrong node (or the triggering element). Match theidyou return. - Testing partials without
HX-Request. A barecurl -X POST /incmay wrap the fragment in a full page. Send the HTMX header (or click in the browser) to see the real swap body. - Global mutable state in production. The demo uses a module dict for clarity. Use a database, session, or scoped store for real apps.
What to do next
- Run the counter locally with
uv pip install python-fasthtmlandpython main.py. - Add a form (
Input,Form,hx_post) that echoes a name into a target div. - Compare the same UI in FastAPI + Jinja if you already have that stack — pick the one your team can ship faster.
- Keep installs/lint fast with uv + Ruff.
FastHTML will not replace every SPA — and it does not try to. For interactive Python tools in 2026, returning HTML from Python is often the shortest path from idea to clickable UI.