Presets Module
The faststrap.presets module provides ready-to-use HTMX interaction patterns and server-side response helpers that eliminate boilerplate for common web interactions. This is the killer feature that makes FastHTML development dramatically easier.
Goal
By the end of this guide, you'll be able to add live search, infinite scroll, auto-refresh, and other dynamic features with single-line Python calls.
Quick Start
from faststrap.presets import ActiveSearch, hx_redirect
# Live search in one line
search = ActiveSearch(endpoint="/search", target="#results")
# Server-side redirect
return hx_redirect("/dashboard")
Interaction Presets
ActiveSearch
Live search with debounced server requests.
Start typing to search...
from faststrap.presets import ActiveSearch
# Search input with 300ms debounce
ActiveSearch(
endpoint="/search",
target="#results",
placeholder="Search products...",
debounce=300
)
# Server endpoint
@app.get("/search")
def search(q: str = ""):
if not q:
return P("Start typing to search...", cls="text-muted")
products = db.query(Product).filter(
Product.name.ilike(f"%{q}%")
).limit(10).all()
return Div(*[
Card(product.name, product.price)
for product in products
])
InfiniteScroll
Infinite feed loading on scroll.
from faststrap.presets import InfiniteScroll
# Feed container
Div(
*initial_posts,
InfiniteScroll(
endpoint="/feed?page=2",
target="#feed",
threshold="200px"
),
id="feed"
)
# Server endpoint
@app.get("/feed")
def get_feed(page: int = 1):
posts = db.query(Post).offset((page-1)*10).limit(10).all()
items = [PostCard(post) for post in posts]
# Add next page loader
if len(posts) == 10:
items.append(
InfiniteScroll(
endpoint=f"/feed?page={page+1}",
target="#feed"
)
)
return Div(*items)
AutoRefresh
Auto-polling for live updates.
from faststrap.presets import AutoRefresh
# Live metrics dashboard
Div(
H3("Server Metrics"),
Div(id="metrics"),
AutoRefresh(
endpoint="/metrics",
target="#metrics",
interval=5000 # 5 seconds
)
)
# Server endpoint
@app.get("/metrics")
def get_metrics():
return Div(
StatCard("CPU", f"{get_cpu_usage()}%"),
StatCard("Memory", f"{get_memory_usage()}%"),
StatCard("Requests", get_request_count())
)
LazyLoad
Lazy-loaded content blocks.
from faststrap.presets import LazyLoad
# Heavy widget loaded on demand
Card(
H4("Analytics"),
LazyLoad(
endpoint="/widgets/analytics",
placeholder=Div("Loading analytics...", cls="text-muted")
)
)
# Server endpoint
@app.get("/widgets/analytics")
def analytics_widget():
# Expensive computation
data = compute_analytics()
return render_chart(data)
LoadingButton
Button with automatic loading state.
from faststrap.presets import LoadingButton
# Automatically shows spinner during request
LoadingButton(
"Save Profile",
endpoint="/profile/save",
target="#profile-form",
loading_text="Saving..."
)
For HTMX-native intersection ratios, pass values like threshold="0.5" instead of a CSS length.
SSEStream
Server‑Sent Events stream helper.
from faststrap.presets import SSEStream, sse_event
@app.get("/api/stream")
async def stream():
async def gen():
yield sse_event("Hello")
return SSEStream(gen())
Response Helpers
hx_redirect
Client-side redirect via HX-Redirect header.
from faststrap.presets import hx_redirect
@app.post("/login")
def login(email: str, password: str):
user = authenticate(email, password)
if user:
req.session["user_id"] = user.id
return hx_redirect("/dashboard")
else:
return ErrorDialog(message="Invalid credentials")
Use 2xx status codes with hx_redirect(). A browser redirect such as 303
is handled before HTMX reads the HX-Redirect header.
hx_refresh
Full page refresh.
from faststrap.presets import hx_refresh
@app.post("/settings/save")
def save_settings(req):
# Save settings
update_settings(req.form)
# Refresh entire page to apply changes
return hx_refresh()
hx_trigger
Trigger client-side events.
from faststrap.presets import hx_trigger
@app.post("/cart/add")
def add_to_cart(product_id: int):
cart.add(product_id)
# Trigger custom event to update cart count
return hx_trigger("cartUpdated")
# Or with event detail
return hx_trigger({
"showToast": {
"message": "Added to cart!",
"variant": "success"
}
})
toast_response
Return content + out-of-band toast.
from faststrap.presets import toast_response
@app.post("/profile/save")
def save_profile(req):
# Save profile
update_profile(req.form)
# Return updated profile + success toast
return toast_response(
content=ProfileCard(req.user),
message="Profile saved successfully!",
variant="success"
)
Auth Decorator
@require_auth
Session-based route protection.
from faststrap.presets import require_auth
@app.get("/dashboard")
@require_auth(login_url="/login")
def dashboard(req):
user = req.session.get("user")
return DashboardLayout(user=user)
# Custom session key
@app.get("/admin")
@require_auth(
login_url="/admin/login",
session_key="admin_id"
)
def admin_panel(req):
return AdminPanel()
# Disable return-url query parameter
@app.get("/premium")
@require_auth(
login_url="/login",
redirect_param=None
)
def premium_feature(req):
return PremiumContent()
@require_auth preserves the return URL as a relative path plus query string so the default redirect flow stays on your own site.
Complete Examples
Live Search with Filters
from faststrap.presets import ActiveSearch
def ProductSearch():
return Div(
Row(
Col(
ActiveSearch(
endpoint="/products/search",
target="#product-results",
placeholder="Search products..."
),
md=8
),
Col(
Select(
Option("All Categories", value=""),
*[Option(cat.name, value=cat.id) for cat in categories],
hx_get="/products/search",
hx_target="#product-results",
hx_include="[name='q']"
),
md=4
)
),
Div(id="product-results", cls="mt-4")
)
@app.get("/products/search")
def search_products(q: str = "", category: str = ""):
query = db.query(Product)
if q:
query = query.filter(Product.name.ilike(f"%{q}%"))
if category:
query = query.filter(Product.category_id == category)
products = query.limit(20).all()
return Div(*[ProductCard(p) for p in products])
Infinite Scroll Feed
from faststrap.presets import InfiniteScroll
@app.get("/")
def home():
posts = get_posts(page=1)
return Container(
H1("Latest Posts"),
Div(
*[PostCard(post) for post in posts],
InfiniteScroll(
endpoint="/posts?page=2",
target="#feed"
),
id="feed"
)
)
@app.get("/posts")
def get_posts_page(page: int = 1):
posts = db.query(Post).offset((page-1)*10).limit(10).all()
items = [PostCard(post) for post in posts]
# Add next loader if more posts exist
if len(posts) == 10:
items.append(
InfiniteScroll(
endpoint=f"/posts?page={page+1}",
target="#feed"
)
)
return Div(*items)
API Reference
Interaction Presets
| Function | Parameters | Description |
|---|---|---|
ActiveSearch |
endpoint, target, placeholder, debounce |
Live search input |
InfiniteScroll |
endpoint, target, threshold |
Infinite scroll loader |
AutoRefresh |
endpoint, target, interval |
Auto-polling element |
LazyLoad |
endpoint, placeholder, trigger |
Lazy-loaded content |
LoadingButton |
text, endpoint, target, method |
Button with loading state |
SSEStream |
events, headers |
Server-Sent Events response |
Response Helpers
| Function | Parameters | Returns | Description |
|---|---|---|---|
hx_redirect |
url: str, status_code: int = 204 |
Response |
Client-side redirect |
hx_refresh |
- | Response |
Full page refresh |
hx_trigger |
event: str \| dict |
Response |
Trigger client event |
hx_reswap |
strategy: str |
Response |
Change swap strategy |
hx_retarget |
selector: str |
Response |
Change target element |
toast_response |
content, message, variant |
Any |
Content + OOB toast |
Auth
| Decorator | Parameters | Description |
|---|---|---|
@require_auth |
login_url, session_key, redirect_param |
Protect routes |
faststrap.presets
Faststrap Presets — HTMX Interaction Helpers.
This module provides ready-to-use HTMX patterns and server-side response helpers that eliminate boilerplate for common web interactions.
Includes: - Interaction presets (ActiveSearch, InfiniteScroll, AutoRefresh, etc.) - Response helpers (hx_redirect, hx_refresh, toast_response, etc.) - Route protection (@require_auth decorator)
ActiveSearch(endpoint, target, debounce=300, placeholder='Search...', name='q', **kwargs)
Live search input with debounced server requests.
Replaces client-side search libraries with pure HTMX server-side filtering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
Server endpoint to send search requests (e.g., "/api/search") |
required |
target
|
str
|
CSS selector for where to render results (e.g., "#results") |
required |
debounce
|
int
|
Milliseconds to wait after typing before sending request |
300
|
placeholder
|
str
|
Input placeholder text |
'Search...'
|
name
|
str
|
Form field name for the search query |
'q'
|
**kwargs
|
Any
|
Additional HTML attributes (cls, id, etc.) |
{}
|
Returns:
| Type | Description |
|---|---|
Input
|
Input element with HTMX search attributes |
Example
Basic usage:
ActiveSearch(endpoint="/search", target="#results")
Custom debounce and styling:
ActiveSearch( ... endpoint="/api/users/search", ... target="#user-list", ... debounce=500, ... placeholder="Search users...", ... cls="form-control-lg" ... )
With additional HTMX attributes:
ActiveSearch( ... endpoint="/search", ... target="#results", ... hx_indicator="#spinner", ... hx_swap="innerHTML" ... )
Note
Uses hx-trigger="keyup changed delay:{debounce}ms" for debouncing.
The server endpoint should accept the search query as a query parameter
with the name specified in the name argument (default: "q").
Source code in src/faststrap/presets/interactions.py
AutoRefresh(endpoint, target, interval=5000, **kwargs)
Auto-refreshing content section.
Polls the server at regular intervals and updates content.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
Server endpoint to poll (e.g., "/api/metrics") |
required |
target
|
str
|
CSS selector for where to render updates (usually self with "this") |
required |
interval
|
int
|
Milliseconds between requests (default: 5000 = 5 seconds) |
5000
|
**kwargs
|
Any
|
Additional HTML attributes |
{}
|
Returns:
| Type | Description |
|---|---|
Div
|
Div element that auto-refreshes its content |
Example
Basic usage (refreshes itself):
AutoRefresh(endpoint="/metrics", target="this", interval=10000)
Refresh specific target:
AutoRefresh( ... endpoint="/api/stats", ... target="#stats-panel", ... interval=3000 ... )
With initial content:
AutoRefresh( ... endpoint="/api/status", ... target="this", ... interval=5000, ... content=Div("Loading status...") ... )
Note
Use target="this" to replace the AutoRefresh element itself.
The server should return HTML that will replace the target content.
Source code in src/faststrap/presets/interactions.py
InfiniteScroll(endpoint, target, trigger='revealed', threshold='0px', **kwargs)
Infinite scroll trigger element.
Loads more content when scrolled into view. Place at the bottom of your list/feed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
Server endpoint to fetch next page (e.g., "/api/feed?page=2") |
required |
target
|
str
|
CSS selector for where to append results (usually same as parent) |
required |
trigger
|
str
|
HTMX trigger event (default: "revealed") |
'revealed'
|
threshold
|
str
|
Intersection observer threshold (default: "0px") |
'0px'
|
**kwargs
|
Any
|
Additional HTML attributes |
{}
|
Returns:
| Type | Description |
|---|---|
Div
|
Div element that triggers loading when scrolled into view |
Example
Basic usage:
InfiniteScroll(endpoint="/feed?page=2", target="#feed")
Custom trigger threshold:
InfiniteScroll( ... endpoint="/api/posts?page=3", ... target="#post-list", ... threshold="200px" # Trigger 200px before visible ... )
With loading indicator:
InfiniteScroll( ... endpoint="/feed?page=2", ... target="#feed", ... hx_indicator="#loading-spinner" ... )
Note
The endpoint should return HTML that will be appended to the target.
Use hx-swap="afterend" to append after the trigger element itself.
Source code in src/faststrap/presets/interactions.py
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | |
LazyLoad(endpoint, trigger='revealed', placeholder=None, **kwargs)
Lazy-loaded content block.
Loads content from server when scrolled into view.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
Server endpoint to fetch content (e.g., "/api/widget") |
required |
trigger
|
str
|
HTMX trigger event (default: "revealed") |
'revealed'
|
placeholder
|
Any | None
|
Content to show before loading (default: "Loading...") |
None
|
**kwargs
|
Any
|
Additional HTML attributes |
{}
|
Returns:
| Type | Description |
|---|---|
Div
|
Div element that loads content on reveal |
Example
Basic usage:
LazyLoad(endpoint="/api/heavy-widget")
Custom placeholder:
LazyLoad( ... endpoint="/api/chart", ... placeholder=Spinner() ... )
Load on click instead of reveal:
LazyLoad( ... endpoint="/api/details", ... trigger="click", ... placeholder=Button("Load Details") ... )
Note
Perfect for below-the-fold content, charts, or heavy components. The server endpoint should return HTML to replace the placeholder.
Source code in src/faststrap/presets/interactions.py
LoadingButton(*children, endpoint, method='post', target=None, variant='primary', **kwargs)
Button with automatic loading state during HTMX requests.
Shows spinner and disables during request. Uses HTMX's built-in hx-indicator
and hx-disabled-elt attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*children
|
Any
|
Button content (text, icons, etc.) |
()
|
endpoint
|
str
|
Server endpoint for the request |
required |
method
|
str
|
HTTP method ("get", "post", "put", "delete") |
'post'
|
target
|
str | None
|
CSS selector for where to render response (optional) |
None
|
variant
|
str
|
Bootstrap button variant |
'primary'
|
**kwargs
|
Any
|
Additional HTML attributes |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
Button component with loading state |
Example
Basic POST button:
LoadingButton("Save", endpoint="/save", target="#form")
GET request with custom variant:
LoadingButton( ... "Load More", ... endpoint="/api/items", ... method="get", ... target="#items", ... variant="outline-primary" ... )
DELETE with confirmation:
LoadingButton( ... "Delete", ... endpoint="/delete/123", ... method="delete", ... variant="danger", ... hx_confirm="Are you sure?" ... )
Note
The button automatically disables during the request and shows
a spinner. No custom JavaScript required.
Requires HTMX 1.9+ for hx-disabled-elt.
Source code in src/faststrap/presets/interactions.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | |
LocationAction(*children, endpoint=None, method='post', target=None, success_event='faststrap:location:success', error_event='faststrap:location:error', variant='secondary', **kwargs)
Progressive location helper with explicit permission/error events.
Behavior:
- Requests browser geolocation on click
- Dispatches bubbling success/error custom events
- Optionally sends coordinates via HTMX (htmx.ajax) when endpoint is set
Source code in src/faststrap/presets/interactions.py
OptimisticAction(*children, endpoint, method='post', target=None, action_id=None, payload=None, apply_event='faststrap:optimistic:apply', commit_event='faststrap:optimistic:commit', rollback_event='faststrap:optimistic:rollback', variant='primary', **kwargs)
Button preset for optimistic UI updates with explicit rollback contract.
Event contract:
- apply_event: fired before request starts
- commit_event: fired after successful response
- rollback_event: fired on failed/network error responses
All events dispatch from the button element with bubbling enabled and
include a detail object:
{actionId, endpoint, method, target, payload, reason}.
Source code in src/faststrap/presets/interactions.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 | |
SSEStream(events, *, headers=None)
Create a StreamingResponse for Server-Sent Events (SSE).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events
|
Iterable[Any] | AsyncIterable[Any]
|
Iterable or async iterable of SSE payloads. |
required |
headers
|
dict[str, str] | None
|
Optional extra headers. |
None
|
Returns:
| Type | Description |
|---|---|
StreamingResponse
|
StreamingResponse configured for text/event-stream. |
Source code in src/faststrap/presets/streams.py
hx_redirect(url, status_code=204)
Return a response that triggers a client-side redirect via HTMX.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
URL to redirect to |
required |
status_code
|
int
|
HTTP status code (default: 204) |
204
|
Returns:
| Type | Description |
|---|---|
Response
|
Response with HX-Redirect header |
Example
After successful form submission:
@app.post("/login") def login(req): # ... authenticate user ... return hx_redirect("/dashboard")
Custom 2xx status:
return hx_redirect("/success", status_code=200)
Note
This triggers a full page redirect on the client side.
HTMX processes HX-Redirect on 2xx responses; 3xx browser redirects
bypass HTMX header handling.
For partial page updates, use regular HTMX responses.
Source code in src/faststrap/presets/responses.py
hx_refresh(status_code=200)
Return a response that triggers a full page refresh via HTMX.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status_code
|
int
|
HTTP status code (default: 200) |
200
|
Returns:
| Type | Description |
|---|---|
Response
|
Response with HX-Refresh header |
Example
After data mutation that affects multiple parts of the page:
@app.post("/update-settings") def update_settings(req): # ... update settings ... return hx_refresh()
Note
Use sparingly. Prefer targeted updates with hx-target when possible.
Source code in src/faststrap/presets/responses.py
hx_reswap(strategy, status_code=200, content='')
Return a response that changes the swap strategy via HTMX.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strategy
|
str
|
Swap strategy (innerHTML, outerHTML, beforebegin, afterbegin, etc.) |
required |
status_code
|
int
|
HTTP status code (default: 200) |
200
|
content
|
str
|
Response content |
''
|
Returns:
| Type | Description |
|---|---|
Response
|
Response with HX-Reswap header |
Example
Change swap strategy dynamically:
@app.get("/widget") def widget(req): if req.query_params.get("replace"): return hx_reswap("outerHTML", content="
New widget") return hx_reswap("innerHTML", content="Widget content")
Note
Valid strategies: innerHTML, outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, none
Source code in src/faststrap/presets/responses.py
hx_retarget(selector, status_code=200, content='')
Return a response that changes the target element via HTMX.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
selector
|
str
|
CSS selector for new target |
required |
status_code
|
int
|
HTTP status code (default: 200) |
200
|
content
|
str
|
Response content |
''
|
Returns:
| Type | Description |
|---|---|
Response
|
Response with HX-Retarget header |
Example
Dynamically change target based on condition:
@app.post("/save") def save(req): if error: return hx_retarget("#error-panel", content=Alert("Error!")) return hx_retarget("#success-panel", content=Alert("Saved!"))
Note
This overrides the hx-target attribute specified in the request.
Source code in src/faststrap/presets/responses.py
hx_trigger(event, detail=None, status_code=200, content='')
Return a response that triggers a client-side event via HTMX.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
str | dict[str, Any]
|
Event name or dict of events with details |
required |
detail
|
Any | None
|
Event detail data (only used if event is a string) |
None
|
status_code
|
int
|
HTTP status code (default: 200) |
200
|
content
|
str
|
Optional response content |
''
|
Returns:
| Type | Description |
|---|---|
Response
|
Response with HX-Trigger header |
Example
Simple event:
return hx_trigger("itemUpdated")
Event with detail:
return hx_trigger("itemUpdated", detail={"id": 123})
Multiple events:
return hx_trigger({ ... "itemUpdated": {"id": 123}, ... "showNotification": {"message": "Saved!"} ... })
Note
Client-side JavaScript can listen for these events:
Source code in src/faststrap/presets/responses.py
require_auth(login_url='/login', session_key='user', redirect_param='next')
Decorator to protect routes with session-based authentication.
Checks if the specified session key exists. If not, redirects to login page. Optionally preserves the original URL as a query parameter for post-login redirect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
login_url
|
str
|
URL to redirect to if not authenticated (default: "/login") |
'/login'
|
session_key
|
str
|
Session key to check for authentication (default: "user") |
'user'
|
redirect_param
|
str | None
|
Query param name for original URL (default: "next", None to disable) |
'next'
|
Returns:
| Type | Description |
|---|---|
Callable
|
Decorator function |
Example
Basic usage:
from faststrap.presets import require_auth
@app.get("/dashboard") @require_auth() def dashboard(req: Request): user = req.session.get("user") return DashboardLayout(f"Welcome, {user['name']}")
Custom login URL and session key:
@app.get("/admin") @require_auth(login_url="/admin/login", session_key="admin_user") def admin_panel(req: Request): return AdminLayout(...)
Disable redirect parameter:
@app.get("/profile") @require_auth(redirect_param=None) def profile(req: Request): return ProfilePage(...)
Note
This decorator only checks for session presence. It does NOT: - Handle login/logout logic - Manage JWT tokens - Validate permissions/roles
For those, implement your own auth service. This just guards the gate.
After login, redirect to the original URL:
Source code in src/faststrap/presets/auth.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
sse_comment(text='keep-alive')
sse_event(data, *, event=None, event_id=None, retry=None)
Build a normalized SSE event payload dict.
Source code in src/faststrap/presets/streams.py
toast_response(content, message, variant='success', toast_id='toast-container', **toast_kwargs)
Return content with an out-of-band toast notification.
This is a killer feature: return your normal HTMX response PLUS a toast notification that appears in a separate part of the page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content
|
Any
|
Main response content (HTML) |
required |
message
|
str
|
Toast message text |
required |
variant
|
str
|
Toast variant (success, danger, warning, info) |
'success'
|
toast_id
|
str
|
ID of the toast container element |
'toast-container'
|
**toast_kwargs
|
Any
|
Additional Toast component kwargs |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
Tuple of (content, toast with hx-swap-oob) |
Example
Success notification after save:
@app.post("/save") def save(req): # ... save logic ... return toast_response( content=Card("Record updated!"), message="Changes saved successfully", variant="success" )
Error notification:
return toast_response( content=Form(...), # Re-render form message="Validation failed", variant="danger" )
Note
Your page must have a toast container element:
The toast will be swapped into this container using HTMX's out-of-band swap feature (hx-swap-oob).