The fastest way to create HTML apps - a next-generation Python web framework for building fast, scalable web applications with minimal code
—
Built-in HTMX support for creating dynamic, interactive web applications without writing JavaScript. FastHTML provides seamless integration with HTMX attributes, response handling, and event management.
Access HTMX-specific request information through structured headers dataclass.
class HtmxHeaders:
"""
HTMX request headers dataclass.
Provides access to HTMX-specific request information.
"""
hx_request: str # Indicates HTMX request
hx_target: str # Target element ID or selector
hx_trigger: str # Element that triggered the request
hx_trigger_name: str # Name of the triggering element
hx_current_url: str # Current page URL
hx_history_restore_request: str # History restore request indicator
hx_prompt: str # User input from hx-prompt
hx_boosted: str # Boosted request indicatorCreate HTML elements with built-in HTMX functionality and attributes.
def ft_hx(tag: str, *c, **kw):
"""
Create HTML element with HTMX support.
Automatically processes HTMX attributes and provides enhanced
functionality for dynamic interactions.
Args:
tag: HTML tag name
*c: Child content
**kw: HTML attributes including HTMX attributes
Returns:
HTML element with HTMX functionality
"""Handle HTMX-specific response requirements and content swapping.
def is_full_page(request) -> bool:
"""
Check if response should be full page or partial.
Determines whether to return a complete HTML page or
just the requested fragment based on HTMX headers.
Args:
request: HTTP request object
Returns:
bool: True if full page response needed
"""
def flat_xt(xt):
"""
Flatten XML tree structure for HTMX responses.
Args:
xt: XML tree or FastHTML element structure
Returns:
Flattened structure suitable for HTMX consumption
"""Pre-defined HTMX constants and configuration options.
htmx_hdrs: dict
"""HTMX header mappings for request processing."""
htmx_resps: dict
"""HTMX response type mappings."""
htmx_exts: dict
"""Available HTMX extensions."""
htmxsrc: str
"""HTMX JavaScript source URL."""
def def_hdrs(htmx: bool = True, surreal: bool = True) -> list:
"""
Get default headers with HTMX/Surreal support.
Args:
htmx: Include HTMX JavaScript library
surreal: Include Surreal.js library
Returns:
list: Default header elements
"""FastHTML supports all HTMX attributes as keyword arguments. Here are the most commonly used:
# HTTP request attributes
hx_get="/path" # GET request to path
hx_post="/path" # POST request to path
hx_put="/path" # PUT request to path
hx_patch="/path" # PATCH request to path
hx_delete="/path" # DELETE request to path
# Target and swapping
hx_target="#element-id" # Target element for response
hx_swap="innerHTML" # How to swap content (innerHTML, outerHTML, etc.)
hx_select="#selector" # Select part of response
# Triggering
hx_trigger="click" # Event that triggers request
hx_trigger="click delay:1s" # Trigger with delay
hx_trigger="every 5s" # Periodic trigger
# Loading states
hx_indicator="#spinner" # Loading indicator element
hx_disabled_elt="this" # Disable element during request
# Form handling
hx_include="#form-data" # Include additional form data
hx_params="*" # Which parameters to include
# History and navigation
hx_push_url="true" # Push URL to browser history
hx_replace_url="true" # Replace current URL
# Validation and confirmation
hx_confirm="Are you sure?" # Confirmation dialog
hx_validate="true" # Client-side validationfrom fasthtml.common import *
app, rt = fast_app()
@rt('/')
def homepage():
return Titled("HTMX Demo",
Div(
H1("HTMX Integration Examples"),
# Simple GET request
Button(
"Load Content",
hx_get="/content",
hx_target="#content-area"
),
Div(id="content-area", "Content will load here"),
# POST form with HTMX
Form(
Input(type="text", name="message", placeholder="Enter message"),
Button("Send", type="submit"),
hx_post="/send-message",
hx_target="#messages",
hx_swap="afterbegin"
),
Div(id="messages")
)
)
@rt('/content')
def load_content():
return Div(
P("This content was loaded dynamically!"),
Small(f"Loaded at {datetime.now()}"),
style="padding: 1rem; border: 1px solid #ccc; margin: 1rem 0;"
)
@rt('/send-message', methods=['POST'])
def send_message(message: str):
return Div(
Strong("New message: "),
Span(message),
style="padding: 0.5rem; background: #e8f5e8; margin: 0.5rem 0;"
)from fasthtml.common import *
app, rt = fast_app()
@rt('/advanced-htmx')
def advanced_examples():
return Titled("Advanced HTMX",
Div(
# Live search with debouncing
Section(
H2("Live Search"),
Input(
type="text",
name="search",
placeholder="Search users...",
hx_get="/search",
hx_target="#search-results",
hx_trigger="keyup changed delay:300ms",
hx_indicator="#search-spinner"
),
Div(id="search-spinner", "🔄", style="display: none;"),
Div(id="search-results")
),
# Infinite scroll
Section(
H2("Infinite Scroll"),
Div(id="content-list",
# Initial content
*[Div(f"Item {i}", cls="list-item") for i in range(10)],
# Load more trigger
Div(
"Loading more...",
hx_get="/load-more?page=2",
hx_target="this",
hx_swap="outerHTML",
hx_trigger="revealed"
)
)
),
# Modal dialog
Section(
H2("Modal Dialog"),
Button(
"Open Modal",
hx_get="/modal",
hx_target="body",
hx_swap="beforeend"
)
),
# Real-time updates
Section(
H2("Real-time Counter"),
Div(id="counter", "0"),
Button(
"Start Updates",
hx_get="/start-counter",
hx_target="#counter",
hx_trigger="click"
)
)
)
)
@rt('/search')
def search_users(search: str = ""):
if not search:
return Div("Enter search term")
# Simulate user search
users = [f"User {i}: {search}" for i in range(1, 6)]
return Div(
*[Div(user, cls="search-result") for user in users]
)
@rt('/load-more')
def load_more(page: int = 1):
start = (page - 1) * 10
items = [Div(f"Item {start + i}", cls="list-item") for i in range(1, 11)]
if page < 5: # Simulate having more pages
load_trigger = Div(
"Loading more...",
hx_get=f"/load-more?page={page + 1}",
hx_target="this",
hx_swap="outerHTML",
hx_trigger="revealed"
)
items.append(load_trigger)
return Div(*items)
@rt('/modal')
def show_modal():
return Div(
Div(
H3("Modal Title"),
P("This is modal content"),
Button(
"Close",
onclick="this.closest('.modal-overlay').remove()"
),
cls="modal-content"
),
cls="modal-overlay",
style="""
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); display: flex;
align-items: center; justify-content: center;
"""
)
@rt('/start-counter')
def start_counter():
return Div(
"1",
hx_get="/increment-counter?count=1",
hx_trigger="load delay:1s",
hx_swap="outerHTML"
)
@rt('/increment-counter')
def increment_counter(count: int = 0):
new_count = count + 1
if new_count <= 10:
return Div(
str(new_count),
hx_get=f"/increment-counter?count={new_count}",
hx_trigger="load delay:1s",
hx_swap="outerHTML"
)
return Div("Finished!")from fasthtml.common import *
app, rt = fast_app()
@rt('/htmx-forms')
def htmx_forms():
return Titled("HTMX Forms",
Div(
# Inline validation
Form(
H2("Registration Form"),
Div(
Label("Username:", for_="username"),
Input(
type="text",
name="username",
id="username",
hx_post="/validate-username",
hx_target="#username-validation",
hx_trigger="blur"
),
Div(id="username-validation"),
cls="form-group"
),
Div(
Label("Email:", for_="email"),
Input(
type="email",
name="email",
id="email",
hx_post="/validate-email",
hx_target="#email-validation",
hx_trigger="blur"
),
Div(id="email-validation"),
cls="form-group"
),
Button("Register", type="submit"),
hx_post="/register",
hx_target="#form-result"
),
Div(id="form-result"),
# Dynamic form fields
Form(
H2("Dynamic Fields"),
Div(id="field-container",
Div(
Input(type="text", name="field1", placeholder="Field 1"),
cls="dynamic-field"
)
),
Button(
"Add Field",
type="button",
hx_post="/add-field",
hx_target="#field-container",
hx_swap="beforeend"
),
Button("Submit", type="submit"),
hx_post="/submit-dynamic",
hx_target="#dynamic-result"
),
Div(id="dynamic-result")
)
)
@rt('/validate-username', methods=['POST'])
def validate_username(username: str):
if len(username) < 3:
return Div("Username too short", style="color: red;")
elif username.lower() in ['admin', 'root', 'user']:
return Div("Username not available", style="color: red;")
else:
return Div("Username available", style="color: green;")
@rt('/validate-email', methods=['POST'])
def validate_email(email: str):
if '@' not in email:
return Div("Invalid email format", style="color: red;")
else:
return Div("Email format valid", style="color: green;")
@rt('/add-field', methods=['POST'])
def add_field():
import random
field_id = random.randint(1000, 9999)
return Div(
Input(
type="text",
name=f"field{field_id}",
placeholder=f"Field {field_id}"
),
Button(
"Remove",
type="button",
onclick="this.closest('.dynamic-field').remove()"
),
cls="dynamic-field"
)from fasthtml.common import *
app, rt = fast_app()
@rt('/error-handling')
def error_handling():
return Titled("Error Handling",
Div(
# Loading states
Button(
"Slow Request",
hx_get="/slow-request",
hx_target="#slow-result",
hx_indicator="#loading-spinner",
hx_disabled_elt="this"
),
Div(id="loading-spinner", "Loading...", style="display: none;"),
Div(id="slow-result"),
# Error handling
Button(
"Request with Error",
hx_get="/error-request",
hx_target="#error-result"
),
Div(id="error-result"),
# Retry mechanism
Button(
"Unreliable Request",
hx_get="/unreliable-request",
hx_target="#retry-result"
),
Div(id="retry-result")
)
)
@rt('/slow-request')
def slow_request():
import time
time.sleep(2) # Simulate slow operation
return Div("Request completed!", style="color: green;")
@rt('/error-request')
def error_request():
# Simulate error condition
return Div(
"An error occurred!",
Button(
"Retry",
hx_get="/error-request",
hx_target="#error-result"
),
style="color: red;"
), 500 # HTTP 500 error
@rt('/unreliable-request')
def unreliable_request():
import random
if random.random() < 0.5:
return Div("Success!", style="color: green;")
else:
return Div(
"Failed, try again",
Button(
"Retry",
hx_get="/unreliable-request",
hx_target="#retry-result"
),
style="color: orange;"
)Install with Tessl CLI
npx tessl i tessl/pypi-python-fasthtml