Skip to content

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.

Live Preview

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
def ActiveSearch(
    endpoint: str,
    target: str,
    debounce: int = 300,
    placeholder: str = "Search...",
    name: str = "q",
    **kwargs: Any,
) -> Input:
    """Live search input with debounced server requests.

    Replaces client-side search libraries with pure HTMX server-side filtering.

    Args:
        endpoint: Server endpoint to send search requests (e.g., "/api/search")
        target: CSS selector for where to render results (e.g., "#results")
        debounce: Milliseconds to wait after typing before sending request
        placeholder: Input placeholder text
        name: Form field name for the search query
        **kwargs: Additional HTML attributes (cls, id, etc.)

    Returns:
        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").
    """
    # Build HTMX attributes
    hx_attrs = {
        "hx_get": endpoint,
        "hx_target": target,
        "hx_trigger": f"keyup changed delay:{debounce}ms",
    }

    # Merge with user-provided HTMX attrs (allow override)
    for key in ["hx_indicator", "hx_swap", "hx_push_url"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    # Build classes
    base_classes = ["form-control"]
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes(" ".join(base_classes), user_cls)

    # Build final attributes
    attrs: dict[str, Any] = {
        "cls": all_classes,
        "type": "search",
        "name": name,
        "placeholder": placeholder,
        **hx_attrs,
    }
    attrs.update(convert_attrs(kwargs))

    return Input(**attrs)
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
def AutoRefresh(
    endpoint: str,
    target: str,
    interval: int = 5000,
    **kwargs: Any,
) -> Div:
    """Auto-refreshing content section.

    Polls the server at regular intervals and updates content.

    Args:
        endpoint: Server endpoint to poll (e.g., "/api/metrics")
        target: CSS selector for where to render updates (usually self with "this")
        interval: Milliseconds between requests (default: 5000 = 5 seconds)
        **kwargs: Additional HTML attributes

    Returns:
        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.
    """
    # Build HTMX attributes
    hx_attrs = {
        "hx_get": endpoint,
        "hx_target": target,
        "hx_trigger": f"every {interval}ms",
        "hx_swap": kwargs.pop("hx_swap", "innerHTML"),
    }

    # Merge with user-provided HTMX attrs
    for key in ["hx_indicator"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    # Build classes
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes("auto-refresh", user_cls)

    # Extract content BEFORE converting remaining attrs
    content = kwargs.pop("content", Div("Loading...", cls="text-muted"))

    # Build final attributes
    attrs: dict[str, Any] = {
        "cls": all_classes,
        **hx_attrs,
    }
    attrs.update(convert_attrs(kwargs))

    return Div(content, **attrs)
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
def InfiniteScroll(
    endpoint: str,
    target: str,
    trigger: str = "revealed",
    threshold: str = "0px",
    **kwargs: Any,
) -> Div:
    """Infinite scroll trigger element.

    Loads more content when scrolled into view. Place at the bottom of your list/feed.

    Args:
        endpoint: Server endpoint to fetch next page (e.g., "/api/feed?page=2")
        target: CSS selector for where to append results (usually same as parent)
        trigger: HTMX trigger event (default: "revealed")
        threshold: Intersection observer threshold (default: "0px")
        **kwargs: Additional HTML attributes

    Returns:
        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.
    """
    normalized_trigger = trigger.strip()
    normalized_threshold = threshold.strip()

    # Build HTMX attributes
    hx_attrs = {
        "hx_get": endpoint,
        "hx_target": target,
        "hx_trigger": normalized_trigger,
        "hx_swap": kwargs.pop("hx_swap", "beforeend"),
    }

    # Add supported threshold handling without breaking existing callers.
    if normalized_threshold != "0px" and normalized_trigger in {"revealed", "intersect"}:
        if _is_numeric_threshold(normalized_threshold):
            hx_attrs["hx_trigger"] = f"intersect once threshold:{normalized_threshold}"
        else:
            hx_attrs["hx_trigger"] = "faststrap:infinite-scroll once"
            existing_data = kwargs.get("data")
            data_attrs: dict[str, Any] = {}
            if isinstance(existing_data, dict):
                data_attrs.update(existing_data)
            kwargs["data"] = {
                **data_attrs,
                "fs_infinite_scroll": True,
                "fs_infinite_margin": normalized_threshold,
            }

    # Merge with user-provided HTMX attrs
    for key in ["hx_indicator", "hx_push_url"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    # Build classes
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes("infinite-scroll-trigger", user_cls)

    # Extract content BEFORE converting remaining attrs
    content = kwargs.pop("content", Div("Loading more...", cls="text-center text-muted py-3"))

    # Build final attributes
    attrs: dict[str, Any] = {
        "cls": all_classes,
        **hx_attrs,
    }
    attrs.update(convert_attrs(kwargs))

    return Div(content, **attrs)
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
def LazyLoad(
    endpoint: str,
    trigger: str = "revealed",
    placeholder: Any | None = None,
    **kwargs: Any,
) -> Div:
    """Lazy-loaded content block.

    Loads content from server when scrolled into view.

    Args:
        endpoint: Server endpoint to fetch content (e.g., "/api/widget")
        trigger: HTMX trigger event (default: "revealed")
        placeholder: Content to show before loading (default: "Loading...")
        **kwargs: Additional HTML attributes

    Returns:
        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.
    """
    # Build HTMX attributes
    hx_attrs = {
        "hx_get": endpoint,
        "hx_trigger": trigger,
        "hx_swap": kwargs.pop("hx_swap", "outerHTML"),
    }

    # Merge with user-provided HTMX attrs
    for key in ["hx_target", "hx_indicator"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    # Build classes
    user_cls = kwargs.pop("cls", "")
    all_classes = merge_classes("lazy-load", user_cls)

    # Build final attributes
    attrs: dict[str, Any] = {
        "cls": all_classes,
        **hx_attrs,
    }
    attrs.update(convert_attrs(kwargs))

    # Default placeholder
    if placeholder is None:
        placeholder = Div("Loading...", cls="text-center text-muted py-3")

    return Div(placeholder, **attrs)
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
def LoadingButton(
    *children: Any,
    endpoint: str,
    method: str = "post",
    target: str | None = None,
    variant: str = "primary",
    **kwargs: Any,
) -> Any:
    """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.

    Args:
        *children: Button content (text, icons, etc.)
        endpoint: Server endpoint for the request
        method: HTTP method ("get", "post", "put", "delete")
        target: CSS selector for where to render response (optional)
        variant: Bootstrap button variant
        **kwargs: Additional HTML attributes

    Returns:
        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`.
    """
    # Build HTMX attributes
    hx_method_attr = f"hx_{method}"
    hx_attrs = {
        hx_method_attr: endpoint,
        "hx_disabled_elt": "this",  # Disable button during request
    }

    if target:
        hx_attrs["hx_target"] = target

    # Default indicator to "this" if not provided
    if "hx_indicator" not in kwargs:
        hx_attrs["hx_indicator"] = "this"

    # Merge with user-provided HTMX attrs
    for key in ["hx_swap", "hx_confirm", "hx_indicator", "hx_push_url"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    # Use the existing Button component
    return Button(
        *children,
        variant=variant,  # type: ignore
        loading=False,  # We'll use hx-indicator instead
        **hx_attrs,  # type: ignore
        **kwargs,
    )
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
def LocationAction(
    *children: Any,
    endpoint: str | None = None,
    method: str = "post",
    target: str | None = None,
    success_event: str = "faststrap:location:success",
    error_event: str = "faststrap:location:error",
    variant: str = "secondary",
    **kwargs: Any,
) -> Any:
    """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
    """
    endpoint_js = json.dumps(endpoint) if endpoint else "null"
    target_js = json.dumps(target) if target else "null"
    method_js = json.dumps(method.upper())
    success_event_js = json.dumps(success_event)
    error_event_js = json.dumps(error_event)

    onclick_script = (
        "if(!navigator.geolocation){"
        f"this.dispatchEvent(new CustomEvent({error_event_js},{{bubbles:true,detail:{{reason:'unsupported'}}}}));"
        "return;"
        "}"
        "navigator.geolocation.getCurrentPosition((pos)=>{"
        "const detail={latitude:pos.coords.latitude,longitude:pos.coords.longitude,accuracy:pos.coords.accuracy};"
        f"this.dispatchEvent(new CustomEvent({success_event_js},{{bubbles:true,detail}}));"
        f"const endpoint={endpoint_js};"
        f"const target={target_js};"
        f"const method={method_js};"
        "if(endpoint && window.htmx){"
        "htmx.ajax(method, endpoint, {target: target || this, values: detail});"
        "}"
        "},(err)=>{"
        f"this.dispatchEvent(new CustomEvent({error_event_js},{{bubbles:true,detail:{{reason:'denied',code:err.code,message:err.message}}}}));"
        "});"
    )

    return Button(
        *children if children else ("Share location",),
        variant=variant,  # type: ignore[arg-type]
        type="button",
        onclick=onclick_script,
        **kwargs,
    )
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
def OptimisticAction(
    *children: Any,
    endpoint: str,
    method: str = "post",
    target: str | None = None,
    action_id: str | None = None,
    payload: dict[str, Any] | None = None,
    apply_event: str = "faststrap:optimistic:apply",
    commit_event: str = "faststrap:optimistic:commit",
    rollback_event: str = "faststrap:optimistic:rollback",
    variant: str = "primary",
    **kwargs: Any,
) -> Any:
    """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}`.
    """
    normalized_method = method.lower()
    if normalized_method not in {"get", "post", "put", "patch", "delete"}:
        msg = f"Unsupported HTTP method for OptimisticAction: {method!r}"
        raise ValueError(msg)

    resolved_action_id = action_id or f"{normalized_method}:{endpoint}"
    resolved_payload: dict[str, Any] = payload or {}

    base_detail = {
        "actionId": resolved_action_id,
        "endpoint": endpoint,
        "method": normalized_method,
        "target": target,
        "payload": resolved_payload,
    }

    before_script = _build_optimistic_dispatch_script(
        apply_event,
        {**base_detail, "reason": "before-request"},
    )
    commit_script = (
        "if(event.detail.successful){"
        + _build_optimistic_dispatch_script(
            commit_event,
            {**base_detail, "reason": "success"},
        )
        + "}"
    )
    rollback_script = _build_optimistic_dispatch_script(
        rollback_event,
        {**base_detail, "reason": "response-error"},
    )
    rollback_send_error_script = _build_optimistic_dispatch_script(
        rollback_event,
        {**base_detail, "reason": "network-error"},
    )

    hx_method_attr = f"hx_{normalized_method}"
    hx_attrs: dict[str, Any] = {
        hx_method_attr: endpoint,
        "hx_disabled_elt": "this",
        "hx-on::before-request": before_script,
        "hx-on::after-request": commit_script,
        "hx-on::response-error": rollback_script,
        "hx-on::send-error": rollback_send_error_script,
    }

    if target:
        hx_attrs["hx_target"] = target

    if "hx_indicator" not in kwargs:
        hx_attrs["hx_indicator"] = "this"

    for key in ["hx_swap", "hx_confirm", "hx_indicator", "hx_push_url"]:
        if key in kwargs:
            hx_attrs[key] = kwargs.pop(key)

    data_attrs = kwargs.pop("data", {})
    if not isinstance(data_attrs, dict):
        data_attrs = {}
    data_attrs = {
        **data_attrs,
        "faststrap_optimistic_id": resolved_action_id,
        "faststrap_optimistic_endpoint": endpoint,
        "faststrap_optimistic_method": normalized_method,
    }
    kwargs["data"] = data_attrs

    return Button(
        *children,
        variant=variant,  # type: ignore[arg-type]
        loading=False,
        **hx_attrs,  # type: ignore[arg-type]
        **kwargs,
    )
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
def SSEStream(
    events: Iterable[Any] | AsyncIterable[Any],
    *,
    headers: dict[str, str] | None = None,
) -> StreamingResponse:
    """Create a StreamingResponse for Server-Sent Events (SSE).

    Args:
        events: Iterable or async iterable of SSE payloads.
        headers: Optional extra headers.

    Returns:
        StreamingResponse configured for text/event-stream.
    """
    base_headers = {
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "X-Accel-Buffering": "no",
    }
    if headers:
        base_headers.update(headers)

    body_iter: Iterable[str] | AsyncIterator[str]
    if isinstance(events, AsyncIterable):
        body_iter = _aiter_sse(events)
    else:
        body_iter = _iter_sse(events)

    return StreamingResponse(body_iter, media_type="text/event-stream", headers=base_headers)
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
def hx_redirect(url: str, status_code: int = 204) -> Response:
    """Return a response that triggers a client-side redirect via HTMX.

    Args:
        url: URL to redirect to
        status_code: HTTP status code (default: 204)

    Returns:
        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.
    """
    return Response(
        content="",
        status_code=status_code,
        headers={"HX-Redirect": url},
    )
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
def hx_refresh(status_code: int = 200) -> Response:
    """Return a response that triggers a full page refresh via HTMX.

    Args:
        status_code: HTTP status code (default: 200)

    Returns:
        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.
    """
    return Response(
        content="",
        status_code=status_code,
        headers={"HX-Refresh": "true"},
    )
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
def hx_reswap(strategy: str, status_code: int = 200, content: str = "") -> Response:
    """Return a response that changes the swap strategy via HTMX.

    Args:
        strategy: Swap strategy (innerHTML, outerHTML, beforebegin, afterbegin, etc.)
        status_code: HTTP status code (default: 200)
        content: Response content

    Returns:
        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="<div>New widget</div>")
        >>>     return hx_reswap("innerHTML", content="Widget content")

    Note:
        Valid strategies: innerHTML, outerHTML, beforebegin, afterbegin,
        beforeend, afterend, delete, none
    """
    return Response(
        content=content,
        status_code=status_code,
        headers={"HX-Reswap": strategy},
    )
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
def hx_retarget(selector: str, status_code: int = 200, content: str = "") -> Response:
    """Return a response that changes the target element via HTMX.

    Args:
        selector: CSS selector for new target
        status_code: HTTP status code (default: 200)
        content: Response content

    Returns:
        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.
    """
    return Response(
        content=content,
        status_code=status_code,
        headers={"HX-Retarget": selector},
    )
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:

document.body.addEventListener("itemUpdated", function(evt){
    console.log(evt.detail);
});

Source code in src/faststrap/presets/responses.py
def hx_trigger(
    event: str | dict[str, Any],
    detail: Any | None = None,
    status_code: int = 200,
    content: str = "",
) -> Response:
    """Return a response that triggers a client-side event via HTMX.

    Args:
        event: Event name or dict of events with details
        detail: Event detail data (only used if event is a string)
        status_code: HTTP status code (default: 200)
        content: Optional response content

    Returns:
        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:
        ```javascript
        document.body.addEventListener("itemUpdated", function(evt){
            console.log(evt.detail);
        });
        ```
    """
    if isinstance(event, str):
        event = _validate_hx_event_name(event)
        if detail is not None:
            trigger_value = json.dumps({event: detail})
        else:
            trigger_value = event
    else:
        for event_name in event:
            _validate_hx_event_name(event_name)
        trigger_value = json.dumps(event)

    return Response(
        content=content,
        status_code=status_code,
        headers={"HX-Trigger": trigger_value},
    )
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:

@app.post("/login")
def login(req: Request):
    # ... authenticate ...
    req.session["user"] = user_data
    next_url = req.query_params.get("next", "/dashboard")
    return RedirectResponse(next_url, status_code=303)

Source code in src/faststrap/presets/auth.py
def require_auth(
    login_url: str = "/login",
    session_key: str = "user",
    redirect_param: str | None = "next",
) -> Callable:
    """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.

    Args:
        login_url: URL to redirect to if not authenticated (default: "/login")
        session_key: Session key to check for authentication (default: "user")
        redirect_param: Query param name for original URL (default: "next", None to disable)

    Returns:
        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:
        ```python
        @app.post("/login")
        def login(req: Request):
            # ... authenticate ...
            req.session["user"] = user_data
            next_url = req.query_params.get("next", "/dashboard")
            return RedirectResponse(next_url, status_code=303)
        ```
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        async def async_wrapper(request: Request, *args: Any, **kwargs: Any) -> Any:
            # Check if user is authenticated
            if session_key not in request.session:
                redirect_to = _build_login_redirect_url(
                    login_url,
                    request=request,
                    redirect_param=redirect_param,
                )
                return RedirectResponse(redirect_to, status_code=303)

            # User is authenticated, proceed
            if asyncio.iscoroutinefunction(func):
                return await func(request, *args, **kwargs)
            return func(request, *args, **kwargs)

        @wraps(func)
        def sync_wrapper(request: Request, *args: Any, **kwargs: Any) -> Any:
            # Check if user is authenticated
            if session_key not in request.session:
                redirect_to = _build_login_redirect_url(
                    login_url,
                    request=request,
                    redirect_param=redirect_param,
                )
                return RedirectResponse(redirect_to, status_code=303)

            # User is authenticated, proceed
            return func(request, *args, **kwargs)

        # Return appropriate wrapper based on function type
        if asyncio.iscoroutinefunction(func):
            return async_wrapper
        return sync_wrapper

    return decorator
sse_comment(text='keep-alive')

Build a comment-only SSE payload (used as a keep-alive ping).

Source code in src/faststrap/presets/streams.py
def sse_comment(text: str = "keep-alive") -> dict[str, Any]:
    """Build a comment-only SSE payload (used as a keep-alive ping)."""
    return {"comment": text}
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
def sse_event(
    data: Any,
    *,
    event: str | None = None,
    event_id: str | None = None,
    retry: int | None = None,
) -> dict[str, Any]:
    """Build a normalized SSE event payload dict."""
    return {
        "data": data,
        "event": event,
        "id": event_id,
        "retry": retry,
    }
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:

ToastContainer(id="toast-container")

The toast will be swapped into this container using HTMX's out-of-band swap feature (hx-swap-oob).

Source code in src/faststrap/presets/responses.py
def toast_response(
    content: Any,
    message: str,
    variant: str = "success",
    toast_id: str = "toast-container",
    **toast_kwargs: Any,
) -> Any:
    """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.

    Args:
        content: Main response content (HTML)
        message: Toast message text
        variant: Toast variant (success, danger, warning, info)
        toast_id: ID of the toast container element
        **toast_kwargs: Additional Toast component kwargs

    Returns:
        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:
        ```python
        ToastContainer(id="toast-container")
        ```

        The toast will be swapped into this container using HTMX's
        out-of-band swap feature (hx-swap-oob).
    """

    # Create toast with OOB swap
    toast = Toast(
        message,
        variant=variant,  # type: ignore
        hx_swap_oob=f"afterbegin:#{toast_id}",
        **toast_kwargs,
    )

    # Return both content and OOB toast
    # HTMX will swap content into target AND toast into container
    if isinstance(content, (list, tuple)):
        return (*content, toast)
    return (content, toast)