---
name: fasthtml
description: Build interactive web apps in pure Python using FastHTML. Use when the user wants to prototype web apps in Python without JavaScript frameworks. FastHTML combines HTMX for interactivity, Starlette for routing, and Python "FastTags" for HTML generation. Ideal for rapid prototyping with real-time updates, forms, charts, and database-backed UIs.
---
# FastHTML Dashboard Development
FastHTML creates server-rendered hypermedia apps in pure Python. It combines Starlette (routing), HTMX (interactivity), and FastTags (HTML generation).
**Key principles:**
- Prefer Python over JS; never use React/Vue/Svelte
- Use `serve()` to run (no `if __name__` needed)
- Return FT components or tuples from handlers
- Use HTMX attributes for dynamic updates without page reloads
## Minimal App
```python
from fasthtml.common import *
app, rt = fast_app()
@rt
def index():
return Titled("Dashboard",
P("Welcome to the dashboard"),
A("View Data", hx_get=data, hx_target="#content"),
Div(id="content"))
@rt
def data():
return Div(H2("Data Loaded"), P("Dynamic content here"))
serve()
```
Run: `python app.py` → Access at `localhost:5001`
## FastTags (FT Components)
HTML elements as Python functions. Positional args = children, keyword args = attributes.
```python
# Basic elements
Div(P("Hello"), cls="container") # cls → class
Input(id="email", type="email", placeholder="Email")
Button("Submit", type="submit", disabled=False) # False omits attr
# Special attributes
Label("Name", _for="name") # _for → for
Div(**{'@click': "handler()"}) # dict unpacking for special chars
# Common patterns
Form(method="post", action=handler)(
Input(name="title", placeholder="Title"),
Button("Save"))
```
## HTMX Interactivity
Add `hx_*` attributes for dynamic behavior without JavaScript:
```python
# GET request, swap inner HTML of #results
Button("Load", hx_get=search, hx_target="#results")
# POST form, swap response into #items after existing content
Form(hx_post=create, hx_target="#items", hx_swap="beforeend")(
Input(name="item"), Button("Add"))
# Common hx_swap values: innerHTML (default), outerHTML, beforeend, afterbegin, delete
# Trigger on events: hx_trigger="click", "change", "keyup delay:500ms"
```
## Dashboard Layout Pattern
```python
from fasthtml.common import *
app, rt = fast_app()
def nav():
return Nav(
A("Dashboard", href=index),
A("Reports", href=reports),
A("Settings", href=settings))
def layout(*content, title="Dashboard"):
return Title(title), Container(nav(), Main(*content))
@rt
def index():
return layout(
H1("Overview"),
Grid(
Card(H3("Users"), P("1,234"), header=None, footer=A("View all", href=users)),
Card(H3("Revenue"), P("$45,678")),
Card(H3("Orders"), P("89"))),
title="Dashboard")
```
## Database-Backed Dashboard (Fastlite)
```python
from fasthtml.common import *
from fastlite import database
db = database('dashboard.db')
class Metric: id:int; name:str; value:float; updated:str
metrics = db.create(Metric, transform=True)
app, rt = fast_app()
@rt
def index():
return Titled("Metrics Dashboard",
Div(id="metrics-list")(*[metric_card(m) for m in metrics()]),
Form(hx_post=add_metric, hx_target="#metrics-list", hx_swap="beforeend")(
Input(name="name", placeholder="Metric name"),
Input(name="value", type="number", placeholder="Value"),
Button("Add Metric")))
def metric_card(m):
return Card(H4(m.name), P(f"{m.value}"), id=f"metric-{m.id}",
footer=Button("Delete", hx_post=delete.to(id=m.id),
hx_target=f"#metric-{m.id}", hx_swap="outerHTML"))
@rt
def add_metric(metric: Metric):
return metric_card(metrics.insert(metric))
@rt
def delete(id: int):
metrics.delete(id)
return "" # Empty response removes element
serve()
```
## Charts with Plotly
```python
app, rt = fast_app(hdrs=[Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js")])
@rt
def index():
data = [{"x": [1,2,3], "y": [4,1,2], "type": "scatter"}]
return Titled("Chart Dashboard",
Div(id="chart"),
Script(f"Plotly.newPlot('chart', {data});"))
```
## Real-Time Updates (SSE)
```python
from fasthtml.common import *
import asyncio
hdrs = (Script(src="https://unpkg.com/htmx-ext-sse@2.2.3/sse.js"),)
app, rt = fast_app(hdrs=hdrs)
shutdown = signal_shutdown()
@rt
def index():
return Titled("Live Dashboard",
Div(hx_ext="sse", sse_connect="/stream", sse_swap="message",
hx_swap="innerHTML", id="live-data"))
async def generate():
import random
while not shutdown.is_set():
yield sse_message(Div(f"Value: {random.randint(1,100)}"))
await asyncio.sleep(1)
@rt
async def stream(): return EventStream(generate())
serve()
```
## Forms and Data Binding
```python
from dataclasses import dataclass
@dataclass
class Settings: theme:str; notifications:bool; limit:int
@rt
def settings():
form = Form(hx_post=save_settings)(
LabelInput("Theme", name="theme"),
CheckboxX(id="notifications", label="Enable notifications"),
LabelInput("Limit", name="limit", type="number"),
Button("Save"))
return Titled("Settings", fill_form(form, current_settings))
@rt
def save_settings(s: Settings):
# s is auto-populated from form data
save_to_db(s)
return Div("Settings saved!", id="message")
```
## MonsterUI for Rich Dashboards
For production dashboards with Tailwind styling, use MonsterUI:
```python
from fasthtml.common import *
from monsterui.all import *
app, rt = fast_app(hdrs=Theme.blue.headers())
@rt
def index():
return Titled("Dashboard",
Grid(
Card(H3("Metric 1"), P("$12,345", cls=TextPresets.bold_lg)),
Card(H3("Metric 2"), P("1,234 users")),
Card(H3("Metric 3"), P("98.5%")), cols=3),
Card(
CardHeader(H3("Recent Activity")),
CardBody(
Table(
Thead(Tr(Th("User"), Th("Action"), Th("Time"))),
Tbody(
Tr(Td("Alice"), Td("Login"), Td("2m ago")),
Tr(Td("Bob"), Td("Purchase"), Td("5m ago")))))))
serve()
```
MonsterUI provides: `Card`, `Grid`, `Table`, `Button` (with `ButtonT.primary/destructive`), `Modal`, `NavBar`, `DivFullySpaced`, `DivCentered`, `LabelInput`, `LabelSelect`, form components, and typography presets.
## Common Patterns Reference
| Pattern | Code |
|---------|------|
| Target by ID | `hx_target="#element-id"` |
| Swap modes | `hx_swap="outerHTML"`, `"beforeend"`, `"afterbegin"`, `"delete"` |
| OOB update | `Div(id="x", hx_swap_oob="true")` updates #x regardless of target |
| Route as href | `A("Link", href=handler)` or `hx_get=handler` |
| With params | `handler.to(id=5)` → `/handler?id=5` |
| Confirm action | `hx_confirm="Are you sure?"` |
| Loading indicator | `hx_indicator="#spinner"` |
| Debounce input | `hx_trigger="keyup changed delay:500ms"` |
## File Structure
```
my_dashboard/
├── main.py # App entry point
├── data/
│ └── app.db # SQLite database
└── static/ # Static files (auto-served)
└── styles.css
```
## Quick Reference
- `fast_app()` - Create app with sensible defaults (includes Pico CSS)
- `fast_app(pico=False, hdrs=[...])` - Custom headers, no Pico
- `Titled(title, *children)` - Page with Title + H1 + Container
- `Card(*c, header=, footer=)` - Card component
- `Grid(*divs)` - CSS grid layout
- `fill_form(form, obj)` - Populate form from object
- `serve()` - Run uvicorn with live reload
For comprehensive API details, see references/api-reference.md.