Skip to content

NotificationCenter

Dropdown notification hub built on Bootstrap dropdowns.


Quick Start

Live Preview
from faststrap import NotificationCenter

NotificationCenter(
    ("New report ready", "/reports/1"),
    ("Server maintenance", "/status"),
    count=2,
)

Items

Each item can be:

  • a (label, href) tuple
  • a plain string
  • a custom FastHTML node
NotificationCenter(
    ("Payment failed", "/billing"),
    "No new alerts",
)

Lazy Load With HTMX

NotificationCenter(
    count=5,
    endpoint="/notifications",
    hx_swap="innerHTML",
)

When endpoint is provided, the dropdown menu is loaded on click.


Customization

  • badge_variant controls the badge color
  • menu_cls and button_cls let you style the dropdown
  • max_items caps visible items
  • empty_text sets the empty state

Security Notes

If you load notifications over HTMX, ensure the endpoint is authenticated and returns only the current user's data.


API Reference

faststrap.components.feedback.notification_center.NotificationCenter(*items, count=None, title='Notifications', endpoint=None, center_id=None, menu_cls=None, button_cls=None, badge_variant=None, empty_text='No notifications', max_items=None, hx_swap='innerHTML', push_url=False, **kwargs)

Notification bell + dropdown menu.

Source code in src/faststrap/components/feedback/notification_center.py
@register(category="feedback", requires_js=True)
@beta
def NotificationCenter(
    *items: Any,
    count: int | None = None,
    title: str = "Notifications",
    endpoint: str | None = None,
    center_id: str | None = None,
    menu_cls: str | None = None,
    button_cls: str | None = None,
    badge_variant: VariantType | None = None,
    empty_text: str = "No notifications",
    max_items: int | None = None,
    hx_swap: str | None = "innerHTML",
    push_url: bool = False,
    **kwargs: Any,
) -> Div:
    """Notification bell + dropdown menu."""
    cfg = resolve_defaults(
        "NotificationCenter",
        badge_variant=badge_variant,
        empty_text=empty_text,
        hx_swap=hx_swap,
        push_url=push_url,
    )
    c_badge_variant = cfg.get("badge_variant", badge_variant) or "danger"
    c_empty_text = cfg.get("empty_text", empty_text)
    c_hx_swap = cfg.get("hx_swap", hx_swap)
    c_push_url = cfg.get("push_url", push_url)

    item_list = list(items)
    if max_items is not None:
        item_list = item_list[:max_items]

    wrapper_id = center_id
    if wrapper_id is None:
        wrapper_id = _unique_center_id(_stable_center_id(title, count, len(item_list)))

    menu_id = f"{wrapper_id}-menu"

    badge = None
    if count is not None:
        badge = Span(
            str(count),
            cls=f"badge bg-{c_badge_variant} position-absolute top-0 start-100 translate-middle",
        )

    toggle_attrs: dict[str, Any] = {
        "cls": merge_classes("position-relative faststrap-notification-toggle", button_cls),
        "data_bs_toggle": "dropdown",
        "aria_expanded": "false",
        "type": "button",
    }

    if endpoint:
        toggle_attrs["hx_get"] = endpoint
        toggle_attrs["hx_target"] = f"#{menu_id}"
        toggle_attrs["hx_swap"] = c_hx_swap
        toggle_attrs["hx_trigger"] = "click"
        if c_push_url:
            toggle_attrs["hx_push_url"] = "true"

    toggle = Button(
        Icon("bell"),
        badge,
        variant="link",
        **toggle_attrs,
    )

    menu_items: list[Any] = []
    if title:
        menu_items.append(Span(title, cls="dropdown-header text-uppercase"))

    if item_list:
        for item in item_list:
            if isinstance(item, tuple) and len(item) == 2:
                label, href = item
                menu_items.append(A(label, href=href, cls="dropdown-item"))
            elif isinstance(item, str):
                menu_items.append(Div(item, cls="dropdown-item-text"))
            else:
                menu_items.append(item)
    else:
        menu_items.append(Div(c_empty_text, cls="dropdown-item-text text-muted"))

    menu = Div(
        *menu_items,
        id=menu_id,
        cls=merge_classes("dropdown-menu dropdown-menu-end faststrap-notification-menu", menu_cls),
    )

    attrs: dict[str, Any] = {
        "cls": merge_classes("dropdown faststrap-notification-center", kwargs.pop("cls", "")),
        "id": wrapper_id,
    }
    attrs.update(convert_attrs(kwargs))

    return Div(toggle, menu, **attrs)