Skip to content

Dropdown

The Dropdown component creates elegant contextual menus that appear on click. Perfect for navigation, actions, settings, and any interface where you need to reveal options without cluttering the UI.

Goal

Master creating dropdown menus, understand Bootstrap's dropdown classes, and build dynamic, accessible navigation—all with zero custom JavaScript.


Quick Start

Live Preview
from faststrap import Dropdown

Dropdown(
    "Edit",
    "Delete",
    "---",  # Divider
    "Archive",
    label="Actions",
    variant="primary"
)

Visual Examples & Use Cases

1. Button Variants - Match Your Design

Dropdowns inherit all button variants for consistent theming.

Live Preview
# Solid variants
Dropdown("Action", "Another action", label="Primary", variant="primary")
Dropdown("Action", "Another action", label="Success", variant="success")

# Outline variants (add outline=True to button via toggle_cls)
Dropdown("Delete", "Remove", label="Danger Outline", variant="danger",
         toggle_cls="btn-outline-danger")

2. Dropdown Directions - Control Where Menus Appear

Control menu placement with the direction parameter. Essential for dropdowns near screen edges.

Live Preview
# Down (default) - menu appears below button
Dropdown("Action", "Another action", label="Dropdown", direction="down")

# Up - menu appears above button (useful for bottom toolbars)
Dropdown("Action", "Another action", label="Dropup", direction="up")

# End (Right) - menu appears to the right
Dropdown("Action", "Another action", label="Dropend", direction="end")

# Start (Left) - menu appears to the left
Dropdown("Action", "Another action", label="Dropstart", direction="start")

When to use each direction:

Direction Bootstrap Class Use Case
down .dropdown Default - most dropdowns
up .dropup Bottom navigation bars, footer actions
end .dropend Sidebar menus, nested navigation
start .dropstart Right-aligned sidebars, RTL layouts

3. Split Button Dropdowns - Action + Options

Combine a primary action with additional options. The left button performs the default action, the right reveals more choices.

Live Preview
# Split button - left button is clickable action
Dropdown(
    "Save and Close",
    "Save as Draft",
    "Save as Template",
    label="Save",
    variant="primary",
    split=True  # ← Creates split button
)

Perfect for: - Save operations with variations - Export with format options - Send with delivery methods - Delete with confirmation levels


4. Menu Alignment & Styling

Customize menu appearance with Bootstrap utility classes.

Live Preview
# Dark menu theme
Dropdown(
    "Action", "Active", "Another action",
    label="Dark Menu",
    variant="info",
    menu_cls="dropdown-menu-dark"
)

# Right-aligned menu
Dropdown(
    "Action", "Another action",
    label="Right Aligned",
    variant="secondary",
    menu_cls="dropdown-menu-end"
)

Practical Functionality

HTMX Integration - Dynamic Actions

Trigger server actions when dropdown items are clicked.

Live Preview
Document.pdf
from faststrap import Dropdown, DropdownItem, Card, Icon
from fasthtml.common import Div, H5, A

@app.get("/")
def home():
    return Card(
        Div(
            H5("Document.pdf", cls="mb-0"),
            Dropdown(
                DropdownItem(
                    Icon("download"), " Download",
                    hx_post="/download/123",
                    hx_target="#status"
                ),
                DropdownItem(
                    Icon("share"), " Share",
                    hx_post="/share/123",
                    hx_target="#status"
                ),
                "---",
                DropdownItem(
                    Icon("trash"), " Delete",
                    hx_post="/delete/123",
                    hx_confirm="Are you sure?",
                    hx_target="#status",
                    cls="text-danger"
                ),
                label="Actions",
                variant="outline-secondary",
                size="sm"
            ),
            cls="d-flex justify-content-between align-items-center"
        ),
        Div(id="status", cls="mt-2")
    )

@app.post("/download/{file_id}")
def download_file(file_id: str):
    return Alert("Download started!", variant="success")

@app.post("/delete/{file_id}")
def delete_file(file_id: str):
    return Alert("File deleted!", variant="danger")

User Profile Dropdown

A common pattern for user menus with profile info, settings, and logout.

from faststrap import Dropdown, DropdownItem, Icon, Badge
from fasthtml.common import Div, Img, Strong, Small

def UserDropdown(user_name: str, user_email: str, avatar_url: str):
    return Dropdown(
        # Custom header with user info
        Div(
            Img(src=avatar_url, cls="rounded-circle me-2", 
                style={"width": "32px", "height": "32px"}),
            Div(
                Strong(user_name),
                Small(user_email, cls="text-muted d-block"),
                cls="d-inline-block"
            ),
            cls="dropdown-header d-flex align-items-center"
        ),
        "---",
        DropdownItem(Icon("person"), " Profile", href="/profile"),
        DropdownItem(Icon("gear"), " Settings", href="/settings"),
        DropdownItem(
            Icon("bell"), " Notifications ",
            Badge("3", variant="danger", cls="ms-auto"),
            href="/notifications"
        ),
        "---",
        DropdownItem(Icon("box-arrow-right"), " Logout", 
                    href="/logout", cls="text-danger"),
        label=Img(src=avatar_url, cls="rounded-circle", 
                 style={"width": "40px", "height": "40px"}),
        variant="link",
        toggle_cls="border-0 p-0"
    )

Bootstrap CSS Classes Explained

Understanding dropdown classes helps you customize menus and troubleshoot issues.

Container Classes

Class Purpose When to Use
.dropdown Base container - Creates dropdown context Default direction (down)
.dropup Upward direction - Menu appears above button Bottom toolbars, footer actions
.dropend Right direction - Menu appears to the right Sidebar menus, nested navigation
.dropstart Left direction - Menu appears to the left Right-aligned sidebars
.btn-group Button group - Required for split buttons When using split=True

Button Classes

Class Purpose Effect
.dropdown-toggle Toggle indicator - Adds caret icon Shows menu can be opened
.dropdown-toggle-split Split button style - Smaller toggle area Used with split buttons
.btn-{variant} Button color - Inherited from Button component primary, success, danger, etc.
.btn-{size} Button size - sm, lg Matches button sizing
Class Purpose When to Use
.dropdown-menu Base menu - Styles the menu container Always present on menu
.dropdown-menu-dark Dark theme - Dark background, light text Dark mode interfaces
.dropdown-menu-end Right align - Aligns menu to right edge Right-side buttons, RTL layouts
.dropdown-menu-lg-end Responsive align - Right align on large screens Responsive navigation

Item Classes

Class Purpose Visual Effect
.dropdown-item Menu item - Styles clickable items Hover effect, proper spacing
.dropdown-item.active Active state - Highlights current selection Blue background
.dropdown-item.disabled Disabled state - Non-clickable item Grayed out, no hover
.dropdown-divider Separator - Visual divider between groups Horizontal line
.dropdown-header Section header - Non-clickable label Muted text, smaller font

Responsive Dropdown Patterns

Make dropdowns work beautifully on mobile and desktop.

Mobile-Friendly Sizes

from faststrap import Dropdown

# Large buttons for touch targets on mobile
Dropdown(
    "Edit Profile",
    "Change Password",
    "Privacy Settings",
    "---",
    "Logout",
    label="Account",
    variant="primary",
    size="lg",  # ← Better for mobile
    cls="w-100"  # ← Full width on mobile
)

Responsive Menu Alignment

# Right-aligned on large screens, left-aligned on small
Dropdown(
    "Action 1",
    "Action 2",
    label="Options",
    menu_cls="dropdown-menu-lg-end"  # ← Responsive alignment
)
from faststrap import Navbar, Dropdown

Navbar(
    brand="MyApp",
    items=[
        Dropdown(
            "Dashboard",
            "Analytics",
            "Reports",
            label="Menu",
            variant="link",  # ← Link style for navbar
            toggle_cls="nav-link"  # ← Navbar link styling
        )
    ]
)

Core Faststrap Features

Global Defaults with set_component_defaults

Set consistent dropdown styling across your entire application.

from faststrap import set_component_defaults, Dropdown

# All dropdowns use secondary variant and small size
set_component_defaults("Dropdown", variant="secondary", size="sm")

# Now all dropdowns inherit these defaults
Dropdown("Edit", "Delete", label="Actions")
# ↑ Automatically has variant="secondary" and size="sm"

# Override when needed
Dropdown("Important Action", "Critical Action", 
         label="Admin", variant="danger", size="lg")
# ↑ Explicitly override defaults

Common Default Patterns:

# Admin panels - prominent dropdowns
set_component_defaults("Dropdown", variant="primary", size="lg")

# Data tables - compact action menus
set_component_defaults("Dropdown", variant="outline-secondary", size="sm")

# Dark theme apps - dark menus
set_component_defaults("Dropdown", menu_cls="dropdown-menu-dark")

Advanced Customization

Custom Menu Items with Icons and Badges

from faststrap import Dropdown, DropdownItem, Icon, Badge

Dropdown(
    DropdownItem(
        Icon("inbox-fill", cls="text-primary me-2"),
        "Inbox ",
        Badge("12", variant="primary"),
        href="/inbox"
    ),
    DropdownItem(
        Icon("star-fill", cls="text-warning me-2"),
        "Favorites ",
        Badge("5", variant="warning"),
        href="/favorites"
    ),
    "---",
    DropdownItem(
        Icon("archive", cls="text-muted me-2"),
        "Archive",
        href="/archive"
    ),
    label="Messages",
    variant="info"
)

Nested Dropdowns (Submenus)

While Bootstrap doesn't natively support nested dropdowns, you can create them with custom CSS:

# Note: Requires custom CSS for .dropdown-submenu
from fasthtml.common import Div, A, Ul, Li

Div(
    Button("Main Menu", cls="btn btn-primary dropdown-toggle",
           data_bs_toggle="dropdown"),
    Ul(
        Li(A("Action", cls="dropdown-item", href="#")),
        Li(
            A("Submenu", cls="dropdown-item dropdown-toggle", href="#"),
            Ul(
                Li(A("Subaction 1", cls="dropdown-item", href="#")),
                Li(A("Subaction 2", cls="dropdown-item", href="#")),
                cls="dropdown-menu dropdown-submenu"
            ),
            cls="dropdown-submenu"
        ),
        cls="dropdown-menu"
    ),
    cls="dropdown"
)

Common Recipes

The "More Actions" Pattern

A common pattern for row actions in tables.

from faststrap import Table, Dropdown, DropdownItem, Icon

Table(
    THead(
        Tr(Th("Name"), Th("Status"), Th("Actions"))
    ),
    TBody(
        Tr(
            Td("John Doe"),
            Td(Badge("Active", variant="success")),
            Td(
                Dropdown(
                    DropdownItem(Icon("eye"), " View", href="/view/1"),
                    DropdownItem(Icon("pencil"), " Edit", href="/edit/1"),
                    "---",
                    DropdownItem(Icon("trash"), " Delete", 
                               href="/delete/1", cls="text-danger"),
                    label=Icon("three-dots-vertical"),
                    variant="link",
                    size="sm",
                    toggle_cls="text-dark"
                )
            )
        )
    )
)

The "Bulk Actions" Pattern

Dropdown for performing actions on multiple selected items.

Dropdown(
    DropdownItem("Export Selected", hx_post="/export", hx_include=".item-checkbox:checked"),
    DropdownItem("Archive Selected", hx_post="/archive", hx_include=".item-checkbox:checked"),
    "---",
    DropdownItem("Delete Selected", hx_post="/delete", hx_include=".item-checkbox:checked",
                cls="text-danger", hx_confirm="Delete selected items?"),
    label="Bulk Actions",
    variant="secondary",
    disabled=True,  # Enable via JavaScript when items selected
    id="bulk-actions-dropdown"
)

Accessibility Best Practices

Faststrap automatically handles accessibility, but here's what's happening:

Automatic Features: - aria-expanded attribute toggles on open/close - role="menu" on dropdown menu - role="menuitem" on dropdown items - Keyboard navigation (Arrow keys, Esc, Enter) - Focus management

Manual Enhancements:

Dropdown(
    "Edit",
    "Delete",
    "Archive",
    label="Actions",
    aria_label="Document actions menu",  # Screen reader description
    id="doc-actions"  # For programmatic control
)

Parameter Reference

Parameter Type Default Description
*items Any Required Menu items (strings, DropdownItem, or "---" for dividers)
label str \| None "Dropdown" Button label text
variant VariantType \| None "primary" Button color variant
size "sm" \| "lg" \| None None Button size
split bool \| None False Use split button style
direction "down" \| "up" \| "start" \| "end" "down" Menu direction
toggle_cls str \| None None Additional classes for toggle button
menu_cls str \| None None Additional classes for dropdown menu
item_cls str \| None None Additional classes for all items
**kwargs Any - Additional HTML attributes (cls, id, hx-, data-)

faststrap.components.navigation.dropdown.Dropdown(*items, label=None, variant=None, size=None, split=None, direction=None, toggle_cls=None, menu_cls=None, item_cls=None, **kwargs)

Bootstrap Dropdown component for contextual menus.

Parameters:

Name Type Description Default
*items Any

Dropdown menu items

()
label str | None

Button label text

None
variant VariantType | None

Bootstrap button variant

None
size str | None

Button size

None
split bool | None

Use split button style

None
direction DirectionType | None

Dropdown direction (down, up, start, end)

None
toggle_cls str | None

Additional classes for the toggle button

None
menu_cls str | None

Additional classes for the dropdown menu

None
item_cls str | None

Additional classes for dropdown items

None
**kwargs Any

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

{}
Source code in src/faststrap/components/navigation/dropdown.py
@register(category="navigation", requires_js=True)
def Dropdown(
    *items: Any,
    label: str | None = None,
    variant: VariantType | None = None,
    size: str | None = None,
    split: bool | None = None,
    direction: DirectionType | None = None,
    toggle_cls: str | None = None,
    menu_cls: str | None = None,
    item_cls: str | None = None,
    **kwargs: Any,
) -> Div:
    """Bootstrap Dropdown component for contextual menus.

    Args:
        *items: Dropdown menu items
        label: Button label text
        variant: Bootstrap button variant
        size: Button size
        split: Use split button style
        direction: Dropdown direction (down, up, start, end)
        toggle_cls: Additional classes for the toggle button
        menu_cls: Additional classes for the dropdown menu
        item_cls: Additional classes for dropdown items
        **kwargs: Additional HTML attributes (cls, id, hx-*, data-*, etc.)
    """
    # Resolve API defaults
    cfg = resolve_defaults(
        "Dropdown",
        label=label,
        variant=variant,
        size=size,
        split=split,
        direction=direction,
        toggle_cls=toggle_cls,
        menu_cls=menu_cls,
        item_cls=item_cls,
    )

    c_label = cfg.get("label", "Dropdown")
    c_variant = cfg.get("variant", "primary")
    c_size = cfg.get("size")
    c_split = cfg.get("split", False)
    c_direction = cfg.get("direction", "down")
    c_toggle_cls = cfg.get("toggle_cls", "")
    c_menu_cls = cfg.get("menu_cls", "")
    c_item_cls = cfg.get("item_cls", "")

    # ---- Container classes ------------------------------------------------ #
    container_classes = []

    container_classes.append(
        {
            "up": "dropup",
            "start": "dropstart",
            "end": "dropend",
            "down": "dropdown",
        }[c_direction]
    )

    if c_split:
        container_classes.append("btn-group")

    # ---- Button classes --------------------------------------------------- #
    btn_classes = ["btn", f"btn-{c_variant}"]
    if c_size:
        btn_classes.append(f"btn-{c_size}")

    btn_class_str = " ".join(btn_classes)

    toggle_id = kwargs.pop("id", "dropdownMenuButton")

    # ---- Build buttons ---------------------------------------------------- #
    buttons: list[Any] = []

    if c_split:
        # Action button (left)
        buttons.append(
            Button(c_label, cls=merge_classes(btn_class_str, c_toggle_cls), type="button")
        )

        # Toggle (right)
        buttons.append(
            Button(
                "",
                cls=merge_classes(btn_class_str, "dropdown-toggle dropdown-toggle-split"),
                type="button",
                id=toggle_id,
                data_bs_toggle="dropdown",
                aria_expanded="false",
            )
        )
    else:
        buttons.append(
            Button(
                c_label,
                cls=merge_classes(btn_class_str, "dropdown-toggle", c_toggle_cls),
                type="button",
                id=toggle_id,
                data_bs_toggle="dropdown",
                aria_expanded="false",
            )
        )

    # ---- Build dropdown items -------------------------------------------- #
    menu_items: list[Any] = []

    for item in items:
        # Check for divider string
        if isinstance(item, str) and item == "---":
            menu_items.append(Li(cls="dropdown-divider"))
            continue

        # Check for hr element
        if hasattr(item, "name") and item.name == "hr":
            menu_items.append(Li(cls="dropdown-divider"))
            continue

        # String -> <a>
        if isinstance(item, str):
            menu_items.append(
                Li(
                    A(
                        item,
                        cls=merge_classes("dropdown-item", c_item_cls),
                        href="#",
                        role="menuitem",
                    )
                )
            )
            continue

        # A / Button elements
        if hasattr(item, "name") and item.name in {"a", "button"}:
            cls = merge_classes("dropdown-item", c_item_cls, item.attrs.get("cls", ""))
            cloned_attrs = {**item.attrs, "cls": cls}
            cloned = item.__class__(*item.children, **cloned_attrs)
            menu_items.append(Li(cloned))
            continue

        # Fallback wrapper
        menu_items.append(Li(item, cls=merge_classes("dropdown-item", c_item_cls)))

    # ---- Build menu -------------------------------------------------------- #
    menu = Ul(
        *menu_items,
        cls=merge_classes("dropdown-menu", c_menu_cls),
        role="menu",
        aria_labelledby=toggle_id,
    )

    # ---- Final container --------------------------------------------------- #
    user_cls = kwargs.pop("cls", "")
    final_container_cls = merge_classes(" ".join(container_classes), user_cls)

    attrs: dict[str, Any] = {"cls": final_container_cls}
    attrs.update(convert_attrs(kwargs))

    return Div(*buttons, menu, **attrs)

faststrap.components.navigation.dropdown.DropdownItem(*children, active=False, disabled=False, **kwargs)

Dropdown item helper.

Source code in src/faststrap/components/navigation/dropdown.py
@register(category="navigation", requires_js=True)
def DropdownItem(
    *children: Any,
    active: bool = False,
    disabled: bool = False,
    **kwargs: Any,
) -> A:
    """Dropdown item helper."""
    classes = ["dropdown-item"]
    if active:
        classes.append("active")
    if disabled:
        classes.append("disabled")

    cls_str = merge_classes(" ".join(classes), kwargs.pop("cls", ""))

    attrs: dict[str, Any] = {
        "cls": cls_str,
        "role": "menuitem",
    }

    if disabled:
        attrs["aria_disabled"] = "true"
        attrs["tabindex"] = "-1"

    if "href" not in kwargs:
        attrs["href"] = "#"

    attrs.update(convert_attrs(kwargs))

    return A(*children, **attrs)