Skip to content

CSS Loaders

Faststrap ships additional CSS-only loaders for interfaces that need more personality than the default Bootstrap Spinner.

Use these for loading states, background jobs, AI generation status, and dashboard refreshes. They are dependency-free and theme-aware.

Quick Start

from faststrap import (
    DotsLoader,
    RingLoader,
    WaveLoader,
    PulseLoader,
    PolygonLoader,
    TypewriterLoader,
    ShadowLoader,
    ProgressRing,
)

DotsLoader(variant="info", label="Searching")
RingLoader(size="48px", variant="success")
WaveLoader(variant="warning")
PulseLoader(size="lg", variant="danger")
PolygonLoader(label="Preparing report")
TypewriterLoader("Generating...")
ShadowLoader("Training...")
ProgressRing(72, variant="success")

Components

Component Purpose
DotsLoader Three bouncing dots.
RingLoader Spinning segmented ring.
WaveLoader Vertical wave bars.
PulseLoader Pulsing circle.
PolygonLoader Shape-shifting geometric loader.
TypewriterLoader Text reveal/typewriter loader.
ShadowLoader Text shadow loading effect.
ProgressRing SVG circular progress indicator.

Common Parameters

Parameter Type Default Description
variant str \| None primary Bootstrap variant color.
label str \| None Loading... Accessible label for screen readers.
size str \| sm \| md \| lg component-specific Loader size.

ProgressRing

ProgressRing(
    42,
    max_value=100,
    variant="primary",
    show_text=True,
    label="Profile completion",
)
Parameter Type Default Description
value int \| float required Current value.
max_value int \| float 100 Maximum value.
show_text bool True Show percentage label in the center.

Accessibility

  • Loader components render role="status" and include visually hidden labels.
  • ProgressRing renders role="progressbar" with aria-valuenow, aria-valuemin, and aria-valuemax.
  • Animations respect prefers-reduced-motion.

API Reference

faststrap.components.feedback.loaders.DotsLoader(*, variant=UNSET, label=UNSET, **kwargs)

Render a three-dot loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def DotsLoader(
    *,
    variant: str | None = UNSET,
    label: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a three-dot loading animation."""
    cfg = resolve_defaults("DotsLoader", variant=variant, label=label)
    c_variant = cfg.get("variant") or "primary"
    c_label = cfg.get("label") or "Loading..."

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-dots-loader", user_cls),
        "role": "status",
        "aria_label": c_label,
    }
    style = _variant_style(c_variant)
    if style:
        attrs["style"] = style
    attrs.update(convert_attrs(kwargs))

    return Div(Div(), Div(), Div(), Span(c_label, cls="visually-hidden"), **attrs)

faststrap.components.feedback.loaders.RingLoader(*, variant=UNSET, size=UNSET, label=UNSET, **kwargs)

Render a spinning ring loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def RingLoader(
    *,
    variant: str | None = UNSET,
    size: str | None = UNSET,
    label: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a spinning ring loading animation."""
    cfg = resolve_defaults("RingLoader", variant=variant, size=size, label=label)
    c_variant = cfg.get("variant") or "primary"
    c_size = cfg.get("size") or "64px"
    c_label = cfg.get("label") or "Loading..."

    user_cls = kwargs.pop("cls", "")
    user_style = kwargs.pop("style", "")
    styles = [f"width: {c_size}; height: {c_size};"]
    variant_style = _variant_style(c_variant)
    if variant_style:
        styles.append(variant_style)
    if isinstance(user_style, str) and user_style.strip():
        styles.append(user_style.strip())

    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-ring-loader", user_cls),
        "style": " ".join(styles),
        "role": "status",
        "aria_label": c_label,
    }
    attrs.update(convert_attrs(kwargs))

    return Div(Div(), Div(), Div(), Div(), Span(c_label, cls="visually-hidden"), **attrs)

faststrap.components.feedback.loaders.WaveLoader(*, variant=UNSET, label=UNSET, **kwargs)

Render a vertical wave loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def WaveLoader(
    *,
    variant: str | None = UNSET,
    label: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a vertical wave loading animation."""
    cfg = resolve_defaults("WaveLoader", variant=variant, label=label)
    c_variant = cfg.get("variant") or "primary"
    c_label = cfg.get("label") or "Loading..."

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-wave-loader", user_cls),
        "role": "status",
        "aria_label": c_label,
    }
    style = _variant_style(c_variant)
    if style:
        attrs["style"] = style
    attrs.update(convert_attrs(kwargs))

    bars = [
        Div(cls="faststrap-wave-bar", style=f"animation-delay: {index * 0.1}s;")
        for index in range(5)
    ]
    return Div(*bars, Span(c_label, cls="visually-hidden"), **attrs)

faststrap.components.feedback.loaders.PulseLoader(*, variant=UNSET, size=UNSET, label=UNSET, **kwargs)

Render a pulsing circle loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def PulseLoader(
    *,
    variant: str | None = UNSET,
    size: LoaderSize | None = UNSET,
    label: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a pulsing circle loading animation."""
    cfg = resolve_defaults("PulseLoader", variant=variant, size=size, label=label)
    c_variant = cfg.get("variant") or "primary"
    c_size = cfg.get("size") or "md"
    c_label = cfg.get("label") or "Loading..."

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-pulse-loader", f"pulse-{c_size}", user_cls),
        "role": "status",
        "aria_label": c_label,
    }
    style = _variant_style(c_variant)
    if style:
        attrs["style"] = style
    attrs.update(convert_attrs(kwargs))
    return Div(Div(cls="faststrap-pulse-circle"), Span(c_label, cls="visually-hidden"), **attrs)

faststrap.components.feedback.loaders.PolygonLoader(*, label=UNSET, **kwargs)

Render a shape-shifting polygon loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def PolygonLoader(*, label: str | None = UNSET, **kwargs: Any) -> Div:
    """Render a shape-shifting polygon loading animation."""
    cfg = resolve_defaults("PolygonLoader", label=label)
    c_label = cfg.get("label") or "Loading..."

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-polygon-loader", user_cls),
        "role": "status",
        "aria_label": c_label,
    }
    attrs.update(convert_attrs(kwargs))
    return Div(Span(c_label, cls="visually-hidden"), **attrs)

faststrap.components.feedback.loaders.TypewriterLoader(text='Loading...', **kwargs)

Render a typewriter-style text loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def TypewriterLoader(text: str = "Loading...", **kwargs: Any) -> Div:
    """Render a typewriter-style text loading animation."""
    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-typewriter-loader", user_cls),
        "data_text": text,
        "role": "status",
        "aria_label": text,
    }
    attrs.update(convert_attrs(kwargs))
    return Div(**attrs)

faststrap.components.feedback.loaders.ShadowLoader(text='Loading...', **kwargs)

Render a bouncing shadow text loading animation.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def ShadowLoader(text: str = "Loading...", **kwargs: Any) -> Div:
    """Render a bouncing shadow text loading animation."""
    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-shadow-loader", user_cls),
        "data_text": text,
        "role": "status",
        "aria_label": text,
    }
    attrs.update(convert_attrs(kwargs))
    return Div(**attrs)

faststrap.components.feedback.loaders.ProgressRing(value, *, max_value=100, size=UNSET, variant=UNSET, show_text=UNSET, label=UNSET, **kwargs)

Render a circular SVG progress indicator.

Source code in src/faststrap/components/feedback/loaders.py
@register(category="feedback")
@beta
def ProgressRing(
    value: int | float,
    *,
    max_value: int | float = 100,
    size: str | None = UNSET,
    variant: str | None = UNSET,
    show_text: bool | None = UNSET,
    label: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a circular SVG progress indicator."""
    cfg = resolve_defaults(
        "ProgressRing",
        size=size,
        variant=variant,
        show_text=show_text,
        label=label,
    )
    c_size = cfg.get("size") or "4rem"
    c_variant = cfg.get("variant") or "primary"
    c_show_text = cfg.get("show_text", True)

    safe_max = max(float(max_value), 1.0)
    percentage = min(100.0, max(0.0, (float(value) / safe_max) * 100.0))
    radius = 45
    circumference = 2 * 3.14159 * radius
    stroke_offset = circumference - (percentage / 100.0) * circumference
    accessible_label = cfg.get("label") or f"{int(percentage)}% complete"

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes("faststrap-progress-ring", user_cls),
        "role": "progressbar",
        "aria_label": accessible_label,
        "aria_valuemin": "0",
        "aria_valuemax": str(max_value),
        "aria_valuenow": str(value),
    }
    attrs.update(convert_attrs(kwargs))

    svg = Svg(
        ft(
            "circle",
            cx="50",
            cy="50",
            r=str(radius),
            fill="none",
            stroke="var(--bs-border-color)",
            stroke_width="8",
        ),
        ft(
            "circle",
            cx="50",
            cy="50",
            r=str(radius),
            fill="none",
            stroke=f"var(--bs-{c_variant})",
            stroke_width="8",
            stroke_dasharray=str(circumference),
            stroke_dashoffset=str(stroke_offset),
            stroke_linecap="round",
            transform="rotate(-90 50 50)",
            cls="faststrap-progress-ring-value",
        ),
        viewBox="0 0 100 100",
        style=f"width: {c_size}; height: {c_size};",
        aria_hidden="true",
    )

    children: list[Any] = [svg]
    if c_show_text:
        children.append(
            Div(
                f"{int(percentage)}%",
                cls="position-absolute top-50 start-50 translate-middle fw-bold",
                style="font-size: 0.875rem;",
            )
        )

    return Div(*children, **attrs)