Skip to content

Spinner

The Spinner component creates animated loading indicators that show users something is happening. Essential for async operations, page loads, and any process that takes time.

Goal

Master creating loading spinners, understand Bootstrap animation classes, and build smooth loading experiences that keep users informed.


Quick Start

Live Preview
Loading...
from faststrap import Spinner

Spinner(variant="primary")

Visual Examples & Use Cases

1. Spinner Types - Border vs Grow

Bootstrap provides two animation styles: border (spinning circle) and grow (pulsing dot).

Live Preview
Loading...
Border (default)
Loading...
Grow
# Border spinner (default) - spinning circle
Spinner(variant="primary", spinner_type="border")

# Grow spinner - pulsing dot
Spinner(variant="success", spinner_type="grow")

When to use each:

Type Animation Best For
border Spinning circle Buttons, inline loading, most cases
grow Pulsing dot Subtle indicators, background processes

2. Color Variants - Match Your Context

Use semantic colors to indicate what's loading.

Live Preview
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Spinner(variant="primary")   # Default actions
Spinner(variant="success")   # Successful operations
Spinner(variant="danger")    # Deletions, critical operations
Spinner(variant="warning")   # Caution operations
Spinner(variant="info")      # Information loading
Spinner(variant="light")     # Dark backgrounds
Spinner(variant="dark")      # Light backgrounds

3. Sizes - Small for Buttons, Large for Pages

Adjust spinner size to match context.

Live Preview
Loading...
Loading...
Loading...
# Small - for buttons
Spinner(variant="primary", size="sm")

# Default - standard loading
Spinner(variant="primary")

# Custom large - for full-page loading
Spinner(variant="primary", style={"width": "3rem", "height": "3rem"})

Practical Functionality

In Buttons - Loading States

Show users their action is processing.

Live Preview
from faststrap import Button, Spinner

# Border spinner in button
Button(
    Spinner(size="sm", variant="light", cls="me-2"),
    "Saving...",
    variant="primary",
    disabled=True
)

# Grow spinner in button
Button(
    Spinner(size="sm", variant="light", spinner_type="grow", cls="me-2"),
    "Processing...",
    variant="success",
    disabled=True
)

# Or use Button's built-in loading state
Button("Save", variant="primary", loading=True, loading_text="Saving...")

Full-Page Loading Overlay

Show a loading screen while content loads.

from faststrap import Spinner, Fx
from fasthtml.common import Div, H4

def LoadingOverlay(message: str = "Loading..."):
    return Div(
        Div(
            Spinner(variant="primary", style={"width": "3rem", "height": "3rem"}),
            H4(message, cls="mt-3 text-muted"),
            cls=f"text-center {Fx.fade_in}"
        ),
        cls="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-75",
        style={"z-index": "9999"},
        id="loading-overlay"
    )

# Use with HTMX
@app.get("/")
def home():
    return Div(
        LoadingOverlay("Loading your dashboard..."),
        Div(id="content"),
        hx_get="/load-content",
        hx_target="#content",
        hx_swap="innerHTML",
        hx_trigger="load"
    )

@app.get("/load-content")
def load_content():
    # Your content loading logic
    return Div("Content loaded!", cls="container")

Inline Loading - Text Replacement

Replace text with spinner during async operations.

from faststrap import Spinner

@app.get("/")
def home():
    return Div(
        Button(
            "Load Data",
            hx_get="/fetch-data",
            hx_target="#data-status",
            hx_swap="innerHTML"
        ),
        Div(id="data-status", cls="mt-3")
    )

@app.get("/fetch-data")
def fetch_data():
    # Show spinner immediately
    return Div(
        Spinner(size="sm", variant="primary", cls="me-2"),
        "Fetching data...",
        hx_get="/data-result",
        hx_trigger="load delay:2s",  # Simulate 2s load
        hx_swap="outerHTML"
    )

@app.get("/data-result")
def data_result():
    return Div("Data loaded successfully!", cls="text-success")

Bootstrap CSS Classes Explained

Core Spinner Classes

Class Purpose Effect
.spinner-border Border animation - Spinning circle Default spinner style
.spinner-grow Grow animation - Pulsing dot Alternative animation
.spinner-border-sm Small border - Reduced size For buttons, inline use
.spinner-grow-sm Small grow - Reduced size For buttons, inline use
.text-{variant} Color - Applies variant color primary, success, danger, etc.

Utility Classes for Spinners

Class Purpose Use Case
.visually-hidden Screen reader only - Hides text visually Accessibility label
.me-2, .ms-2 Margin - Spacing around spinner Inline with text
.d-inline-block Display - Inline positioning Inline spinners
.position-absolute Positioning - Absolute placement Overlay spinners

Custom Sizing

# Custom size via style
Spinner(
    variant="primary",
    style={"width": "5rem", "height": "5rem"}
)

# Or via CSS classes
Spinner(
    variant="success",
    cls="spinner-border-lg"  # If you define this in your CSS
)

Responsive Loading Patterns

Mobile-Friendly Loading

from faststrap import Spinner, Card

# Larger spinners for better visibility on mobile
Card(
    Div(
        Spinner(
            variant="primary",
            style={"width": "3rem", "height": "3rem"},
            cls="mb-3"
        ),
        H5("Loading your content...", cls="text-muted"),
        cls="text-center py-5"
    )
)

Skeleton Loading (Alternative Pattern)

While not a spinner, skeleton screens are modern loading patterns:

from fasthtml.common import Div

def SkeletonCard():
    return Div(
        Div(cls="placeholder-glow"),
        Div(cls="placeholder col-12 mb-2"),
        Div(cls="placeholder col-8"),
        cls="card-body"
    )

Core Faststrap Features

Global Defaults with set_component_defaults

Set consistent spinner styling across your app.

from faststrap import set_component_defaults, Spinner

# All spinners use success variant and grow animation
set_component_defaults("Spinner", variant="success", spinner_type="grow")

# Now all spinners inherit these defaults
Spinner()  # ← Automatically success + grow

# Override when needed
Spinner(variant="danger", spinner_type="border")

Common Default Patterns:

# Loading-heavy apps - consistent primary spinners
set_component_defaults("Spinner", variant="primary")

# Dark theme apps - light spinners
set_component_defaults("Spinner", variant="light")

# Subtle loading - grow animations
set_component_defaults("Spinner", spinner_type="grow")

Common Recipes

The "Loading Button" Pattern

Disable button and show spinner during async operations.

from faststrap import Button, Spinner

@app.get("/")
def home():
    return Button(
        "Submit Form",
        id="submit-btn",
        variant="primary",
        hx_post="/submit",
        hx_target="#result",
        hx_indicator="#submit-btn"  # Show loading on this button
    )

# HTMX automatically adds .htmx-request class during requests
# Style it in CSS:
"""
.htmx-request .htmx-indicator {
    display: inline-block !important;
}
.htmx-indicator {
    display: none;
}
"""

# Or manually control with JavaScript/HTMX events

The "Infinite Scroll" Spinner

Show spinner at bottom while loading more content.

@app.get("/")
def home():
    return Div(
        Div(id="items", *[item_card(i) for i in range(20)]),
        Div(
            Spinner(variant="primary"),
            id="load-more",
            hx_get="/load-more?page=2",
            hx_trigger="revealed",  # Triggers when scrolled into view
            hx_swap="outerHTML"
        ),
        cls="container"
    )

@app.get("/load-more")
def load_more(page: int):
    items = [item_card(i) for i in range(page*20, (page+1)*20)]
    next_spinner = Div(
        Spinner(variant="primary"),
        id="load-more",
        hx_get=f"/load-more?page={page+1}",
        hx_trigger="revealed",
        hx_swap="outerHTML"
    )
    return Div(*items, next_spinner)

Accessibility Best Practices

Faststrap automatically handles accessibility:

Automatic Features: - role="status" for screen readers - .visually-hidden text for context - Proper ARIA attributes

Manual Enhancements:

Spinner(
    variant="primary",
    label="Loading user data",  # Custom screen reader text
    aria_live="polite",         # Announce when appears
    aria_busy="true"            # Indicate busy state
)

Parameter Reference

Parameter Type Default Description
variant VariantType \| None "primary" Color variant
size "sm" \| None None Spinner size (default is medium)
spinner_type "border" \| "grow" "border" Animation type
label str \| None "Loading..." Screen reader label
**kwargs Any - Additional HTML attributes (cls, style, id)

faststrap.components.feedback.spinner.Spinner(variant=None, size=None, spinner_type=None, label=None, **kwargs)

Bootstrap Spinner component for loading indicators.

Parameters:

Name Type Description Default
variant VariantType | None

Bootstrap color variant

None
size str | None

Spinner size (e.g., "sm")

None
spinner_type str | None

Spinner animation type ("border" or "grow")

None
label str | None

Screen reader label text

None
**kwargs Any

Additional HTML attributes (cls, id, hx-, data-, etc.)

{}

Returns:

Type Description
Div

Div element with spinner animation

Source code in src/faststrap/components/feedback/spinner.py
@register(category="feedback")
def Spinner(
    variant: VariantType | None = None,
    size: str | None = None,
    spinner_type: str | None = None,
    label: str | None = None,
    **kwargs: Any,
) -> Div:
    """Bootstrap Spinner component for loading indicators.

    Args:
        variant: Bootstrap color variant
        size: Spinner size (e.g., "sm")
        spinner_type: Spinner animation type ("border" or "grow")
        label: Screen reader label text
        **kwargs: Additional HTML attributes (cls, id, hx-*, data-*, etc.)

    Returns:
        Div element with spinner animation
    """
    # Resolve API defaults
    cfg = resolve_defaults(
        "Spinner", variant=variant, size=size, spinner_type=spinner_type, label=label
    )

    c_variant = cfg.get("variant", "primary")
    c_size = cfg.get("size")
    c_type = cfg.get("spinner_type", "border")
    c_label = cfg.get("label", "Loading...")

    # Build spinner classes
    classes = [f"spinner-{c_type}"]

    if c_variant:
        classes.append(f"text-{c_variant}")

    if c_size == "sm":
        classes.append(f"spinner-{c_type}-sm")

    # Merge with user classes
    user_cls = kwargs.pop("cls", "")
    cls = merge_classes(" ".join(classes), user_cls)

    # Build attributes
    attrs: dict[str, Any] = {
        "cls": cls,
        "role": "status",
    }

    # Convert remaining kwargs
    attrs.update(convert_attrs(kwargs))

    # Screen reader text
    sr_text = Span(c_label, cls="visually-hidden")

    return Div(sr_text, **attrs)