Skip to content

ModernToast

ModernToast is an opinionated alternative to the core Bootstrap Toast. It is for polished app-style notifications with configurable position, timing, style, and queue behavior.

Use core Toast when you want Bootstrap's native toast structure. Use ModernToast when you want a more modern product UI surface.

Import

from faststrap import ModernToast, ModernToastStack, ToastPlacement

Basic Usage

ModernToast(
    "Saved",
    "Your changes were applied.",
    intent="success",
)

With Placement, Duration, And Style

ModernToast(
    "Invite sent",
    "We emailed the new team member.",
    intent="success",
    placement=ToastPlacement(position="top-end"),
    duration=4000,
    visual_style="glass",
)

Toast Stack

ModernToastStack(
    ModernToast("Saved", intent="success"),
    ModernToast("Sync delayed", "Trying again soon.", intent="warning"),
    placement=ToastPlacement(position="bottom-end"),
)

With Action

ModernToast(
    "Project archived",
    "You can restore it from settings.",
    intent="warning",
    action={"label": "Undo", "style": "btn-link"},
)

Parameters

ModernToast

Param Type Default Description
title str required Main toast title.
message str \| None None Optional message.
intent ToastIntent info Semantic intent: success, error, warning, info, loading.
visual_style ToastStyle glass Visual style: solid, soft, glass, minimal.
placement ToastPlacement \| None bottom-right Position + offset + gutter.
duration int \| "infinite" 4000 Auto-dismiss duration in milliseconds, or "infinite" to persist.
icon str \| None None Bootstrap icon override.
action ToastAction \| Any \| None None Optional action button or component.
cancel ToastAction \| Any \| None None Optional cancel button or component.
dismissible bool True Shows the close button.
pause_on_hover bool True Pause auto-dismiss timer on hover/focus.
animation ToastAnimation slide Enter/exit animation, now visually distinct for each value: slide, fade, zoom, none.
radius str \| None None Radius token overriding the default rounded-4: sm, md, lg, none.
shadow str \| None None Shadow token overriding the default shadow-lg: sm, md, lg, none.
title_cls / message_cls / close_button_cls str "" Extra classes for the title, message, and close button.
**kwargs Any Extra attributes.

ModernToastStack

Param Type Default Description
*toasts Any Toast children.
placement ToastPlacement \| None bottom-right Stack position + offset + gutter.
gap int 2 Bootstrap grid gap utility suffix.
max_visible int 5 Maximum toasts visible at once; extras are queued (hidden) until an open toast dismisses.
**kwargs Any Extra attributes.

Behavior Notes

  • ModernToast uses a dedicated JS runtime (modern-toast.js) for auto-dismiss, keyboard dismiss, pause-on-hover, and swipe-to-dismiss.
  • The runtime is loaded automatically when ModernToast or ModernToastStack is used with add_bootstrap(app, components=[ModernToast, ...]).
  • variant, position, and style parameters are deprecated but still accepted with DeprecationWarning. Use intent, placement, and visual_style instead.
  • intent controls both the semantic role (status/alert) and the Bootstrap color classes: error renders as danger and loading as primary (Bootstrap has no error/loading color names).
  • The default surface uses rounded-4 + shadow-lg; for dense dashboards prefer ModernToast(..., radius="md", shadow="sm").
  • Global defaults are supported: set_component_defaults("ModernToast", intent="info", duration=4000).
  • Close buttons, auto-dismiss, keyboard dismiss (Escape while focused), and swipe-to-dismiss are handled by the runtime; no extra wiring needed.
  • animation values produce distinct enter/exit motions: slide, fade, zoom, or none.
  • ModernToastStack(max_visible=...) queues toasts beyond the limit and reveals them as older toasts dismiss.

faststrap.components.feedback.modern_toast.ModernToast(title, message=None, *, intent=UNSET, visual_style='glass', placement=None, duration=UNSET, icon=None, action=None, cancel=None, dismissible=True, pause_on_hover=True, animation='slide', variant=UNSET, position=UNSET, style=UNSET, on_dismiss=None, radius=None, shadow=None, title_cls='', message_cls='', close_button_cls='', **kwargs)

Render an opinionated modern toast surface.

Parameters:

Name Type Description Default
title str

Primary toast heading.

required
message str | None

Optional supporting body text.

None
intent ToastIntent | str | None

Semantic intent driving color and live-region role.

UNSET
visual_style ToastStyle

Surface treatment (solid, soft, glass, minimal).

'glass'
placement ToastPlacement | None

Positioned placement (position/offset/gutter).

None
duration int | Literal['infinite'] | None

Auto-dismiss delay in ms, or "infinite" to disable.

UNSET
icon str | None

Bootstrap icon name override.

None
action ToastAction | Any | None

Optional action button spec or custom element.

None
cancel ToastAction | Any | None

Optional cancel button spec or custom element.

None
dismissible bool

Render a close button.

True
pause_on_hover bool

Pause the auto-dismiss timer while hovered/focused.

True
animation ToastAnimation

Enter/exit animation style.

'slide'
radius RadiusToken | str | None

Radius token overriding the default rounded-4 (e.g. md for a calmer surface in dense dashboards).

None
shadow ShadowToken | str | None

Shadow token overriding the default shadow-lg.

None
title_cls str

Extra classes for the title line.

''
message_cls str

Extra classes for the message paragraph.

''
close_button_cls str

Extra classes for the dismiss button.

''
**kwargs Any

Additional HTML attributes (cls, css_vars, hx-*, ...).

{}
Source code in src/faststrap/components/feedback/modern_toast.py
@register(category="feedback")
@beta
def ModernToast(
    title: str,
    message: str | None = None,
    *,
    intent: ToastIntent | str | None = UNSET,
    visual_style: ToastStyle = "glass",
    placement: ToastPlacement | None = None,
    duration: int | Literal["infinite"] | None = UNSET,
    icon: str | None = None,
    action: ToastAction | Any | None = None,
    cancel: ToastAction | Any | None = None,
    dismissible: bool = True,
    pause_on_hover: bool = True,
    animation: ToastAnimation = "slide",
    variant: str | None = UNSET,
    position: str | None = UNSET,
    style: str | None = UNSET,
    on_dismiss: Any | None = None,
    radius: RadiusToken | str | None = None,
    shadow: ShadowToken | str | None = None,
    title_cls: str = "",
    message_cls: str = "",
    close_button_cls: str = "",
    **kwargs: Any,
) -> Div:
    """Render an opinionated modern toast surface.

    Args:
        title: Primary toast heading.
        message: Optional supporting body text.
        intent: Semantic intent driving color and live-region role.
        visual_style: Surface treatment (``solid``, ``soft``, ``glass``,
            ``minimal``).
        placement: Positioned placement (position/offset/gutter).
        duration: Auto-dismiss delay in ms, or ``"infinite"`` to disable.
        icon: Bootstrap icon name override.
        action: Optional action button spec or custom element.
        cancel: Optional cancel button spec or custom element.
        dismissible: Render a close button.
        pause_on_hover: Pause the auto-dismiss timer while hovered/focused.
        animation: Enter/exit animation style.
        radius: Radius token overriding the default ``rounded-4``
            (e.g. ``md`` for a calmer surface in dense dashboards).
        shadow: Shadow token overriding the default ``shadow-lg``.
        title_cls: Extra classes for the title line.
        message_cls: Extra classes for the message paragraph.
        close_button_cls: Extra classes for the dismiss button.
        **kwargs: Additional HTML attributes (``cls``, ``css_vars``, hx-*, ...).
    """
    resolved_intent = intent
    if variant is not UNSET and variant is not None:
        warnings.warn(
            "ModernToast(variant=...) is deprecated; use intent=... instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        resolved_intent = _VARIANT_TO_INTENT.get(variant, intent)

    resolved_position = "bottom-right"
    if placement is not None:
        resolved_position = placement.position
    elif position is not UNSET and position is not None:
        warnings.warn(
            "ModernToast(position=...) is deprecated; use placement=ToastPlacement(position=...) instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        resolved_position = _POSITION_ALIASES.get(position, position)

    resolved_style: str = visual_style
    if style is not UNSET and style is not None:
        warnings.warn(
            "ModernToast(style=...) is deprecated; use visual_style=... instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        resolved_style = style

    # Global defaults support (set_component_defaults("ModernToast", ...))
    cfg = resolve_defaults("ModernToast", intent=resolved_intent, duration=duration)
    resolved_intent = str(cfg.get("intent") or "info")
    c_bootstrap_color = _INTENT_TO_BOOTSTRAP_COLOR.get(resolved_intent, resolved_intent)
    resolved_duration = cfg.get("duration")
    if resolved_duration is None:
        resolved_duration = 4000

    duration_ms = 0 if resolved_duration == "infinite" else int(resolved_duration)

    user_cls = kwargs.pop("cls", "")
    resolved_icon = icon if icon is not None else TOAST_ICONS.get(resolved_intent)
    base_radius_cls = radius_class(radius) or "rounded-4"
    base_shadow_cls = shadow_class(shadow) or "shadow-lg"
    attrs: dict[str, Any] = {
        "cls": merge_classes(
            "faststrap-modern-toast d-flex gap-3 border p-3",
            base_radius_cls,
            base_shadow_cls,
            f"faststrap-modern-toast-{resolved_style}",
            f"border-{c_bootstrap_color}",
            user_cls,
        ),
        "role": "status" if resolved_intent not in {"danger", "warning", "error"} else "alert",
        "data_fs_modern_toast": "true",
        "data_fs_intent": resolved_intent,
        "data_fs_position": resolved_position,
        "data_fs_duration": str(duration_ms),
        "data_fs_animation": animation,
        "data_fs_pause_on_hover": "true" if pause_on_hover else "false",
    }
    if dismissible:
        attrs["data_fs_dismiss"] = "true"
    attrs.update(convert_attrs(kwargs))

    parts: list[Any] = []
    if resolved_icon:
        parts.append(
            Span(
                Icon(resolved_icon, cls=f"text-{c_bootstrap_color}", aria_hidden="true"),
                cls="fs-5 lh-1 mt-1",
            )
        )

    body_parts: list[Any] = [Span(title, cls=merge_classes("fw-semibold d-block", title_cls))]
    if message:
        body_parts.append(P(message, cls=merge_classes("small text-muted mb-0", message_cls)))
    if action:
        if isinstance(action, dict):
            action_label = action.get("label", "")
            action_style = _ACTION_STYLE_CLASSES.get(
                action.get("style", "link"), action.get("style", "link")
            )
            action_cls = merge_classes(
                "btn", action_style, "disabled" if action.get("loading") else None
            )
            action_btn = FTButton(
                action_label,
                type="button",
                cls=action_cls,
                aria_label=action_label,
            )
            body_parts.append(Div(action_btn, cls="mt-2"))
        else:
            body_parts.append(Div(action, cls="mt-2"))
    if cancel:
        if isinstance(cancel, dict):
            cancel_label = cancel.get("label", "")
            cancel_style = _ACTION_STYLE_CLASSES.get(
                cancel.get("style", "link"), cancel.get("style", "link")
            )
            cancel_cls = merge_classes(
                "btn", cancel_style, "disabled" if cancel.get("loading") else None
            )
            cancel_btn = FTButton(
                cancel_label,
                type="button",
                cls=cancel_cls,
                aria_label=cancel_label,
            )
            body_parts.append(Div(cancel_btn, cls="mt-2"))
        else:
            body_parts.append(Div(cancel, cls="mt-2"))
    parts.append(Div(*body_parts, cls="flex-grow-1 min-w-0"))

    if dismissible:
        parts.append(
            FTButton(
                type="button",
                cls=merge_classes("btn-close", close_button_cls),
                aria_label="Dismiss notification",
                data_fs_dismiss="true",
            )
        )

    return Div(*parts, **attrs)

faststrap.components.feedback.modern_toast.ModernToastStack(*toasts, placement=None, gap=8, max_visible=5, position=UNSET, **kwargs)

Render a positioned stack of ModernToast components.

Parameters:

Name Type Description Default
*toasts Any

Toast children.

()
placement ToastPlacement | None

Stack position/offset/gutter.

None
gap int

Bootstrap gap utility suffix; used when placement.gutter is left at its default (the default 8 preserves the historical 2rem spacing).

8
max_visible int

Toasts visible at once; extras queue (hidden) until others dismiss.

5
position str | None

Deprecated; use placement instead.

UNSET
**kwargs Any

Additional HTML attributes.

{}
Source code in src/faststrap/components/feedback/modern_toast.py
@register(category="feedback")
@beta
def ModernToastStack(
    *toasts: Any,
    placement: ToastPlacement | None = None,
    gap: int = 8,
    max_visible: int = 5,
    position: str | None = UNSET,
    **kwargs: Any,
) -> Div:
    """Render a positioned stack of ModernToast components.

    Args:
        *toasts: Toast children.
        placement: Stack position/offset/gutter.
        gap: Bootstrap gap utility suffix; used when ``placement.gutter`` is
            left at its default (the default ``8`` preserves the historical
            2rem spacing).
        max_visible: Toasts visible at once; extras queue (hidden) until
            others dismiss.
        position: Deprecated; use ``placement`` instead.
        **kwargs: Additional HTML attributes.
    """
    if position is not UNSET and position is not None:
        warnings.warn(
            "ModernToastStack(position=...) is deprecated; use placement=ToastPlacement(position=...) instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        normalized = _POSITION_ALIASES.get(position, position)
        placement = ToastPlacement(position=normalized)  # type: ignore[arg-type]

    resolved_placement = placement or ToastPlacement()
    user_cls = kwargs.pop("cls", "")
    position_classes = {
        "top-start": "top-0 start-0",
        "top-center": "top-0 start-50 translate-middle-x",
        "top-end": "top-0 end-0",
        "top-left": "top-0 start-0",
        "top-right": "top-0 end-0",
        "bottom-start": "bottom-0 start-0",
        "bottom-center": "bottom-0 start-50 translate-middle-x",
        "bottom-end": "bottom-0 end-0",
        "bottom-left": "bottom-0 start-0",
        "bottom-right": "bottom-0 end-0",
        "middle-start": "top-50 start-0 translate-middle-y",
        "middle-center": "top-50 start-50 translate-middle",
        "middle-end": "top-50 end-0 translate-middle-y",
    }
    offset_style = f"margin: {resolved_placement.offset}px;"
    gap_cls = f"gap-{gap}"
    if resolved_placement.gutter != 8:
        # An explicit gutter (px) takes precedence over the gap utility.
        gap_cls = ""
        offset_style += f" gap: {resolved_placement.gutter * 0.25}rem;"
    attrs: dict[str, Any] = {
        "cls": merge_classes(
            "faststrap-modern-toast-stack position-fixed p-3 d-grid",
            gap_cls,
            position_classes.get(resolved_placement.position, position_classes["bottom-right"]),
            user_cls,
        ),
        "data_fs_modern_toast_stack": "true",
        "data_fs_position": resolved_placement.position,
        "data_fs_max_visible": str(max_visible),
        "data_fs_gutter": str(resolved_placement.gutter),
        "style": f"z-index: var(--fs-toast-z-index, 1080); {offset_style}",
    }
    attrs.update(convert_attrs(kwargs))
    return Div(*toasts, **attrs)