# Dropdown Component ## Introduction The Dropdown component provides an interactive dropdown menu that toggles open or closed when clicking a trigger button. It handles positioning, automatic closing behavior, and keyboard navigation out of the box. **Key features:** - Toggle open/close on button click - Automatic close when clicking outside - Keyboard support (ESC to close) - Configurable vertical position (above or below the button) - Configurable horizontal alignment (left, right, or center) - Session-based state management - HTMX-powered updates without page reload **Common use cases:** - Navigation menus - User account menus - Action menus (edit, delete, share) - Filter or sort options - Context-sensitive toolbars - Settings quick access ## Quick Start Here's a minimal example showing a dropdown menu with navigation links: ```python from fasthtml.common import * from myfasthtml.controls.Dropdown import Dropdown from myfasthtml.core.instances import RootInstance # Create root instance and dropdown root = RootInstance(session) dropdown = Dropdown( parent=root, button=Button("Menu", cls="btn"), content=Ul( Li(A("Home", href="/")), Li(A("Settings", href="/settings")), Li(A("Logout", href="/logout")) ) ) # Render the dropdown return dropdown ``` This creates a complete dropdown with: - A "Menu" button that toggles the dropdown - A list of navigation links displayed below the button - Automatic closing when clicking outside the dropdown - ESC key support to close the dropdown **Note:** The dropdown opens below the button and aligns to the left by default. Users can click anywhere outside the dropdown to close it, or press ESC on the keyboard. ## Basic Usage ### Visual Structure The Dropdown component consists of a trigger button and a content panel: ``` Closed state: ┌──────────────┐ │ Button ▼ │ └──────────────┘ Open state (position="below", align="left"): ┌──────────────┐ │ Button ▼ │ ├──────────────┴─────────┐ │ Dropdown Content │ │ - Option 1 │ │ - Option 2 │ │ - Option 3 │ └────────────────────────┘ Open state (position="above", align="right"): ┌────────────────────────┐ │ Dropdown Content │ │ - Option 1 │ │ - Option 2 │ ├──────────────┬─────────┘ │ Button ▲ │ └──────────────┘ ``` **Component details:** | Element | Description | |-----------|------------------------------------------------| | Button | Trigger element that toggles the dropdown | | Content | Panel containing the dropdown menu items | | Wrapper | Container with relative positioning for anchor | ### Creating a Dropdown The Dropdown is a `MultipleInstance`, meaning you can create multiple independent dropdowns in your application. Create it by providing a parent instance: ```python dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content) # Or with a custom ID dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content, _id="my-dropdown") ``` ### Button and Content The dropdown requires two main elements: **Button:** The trigger element that users click to toggle the dropdown. ```python # Simple text button dropdown = Dropdown( parent=root, button=Button("Click me", cls="btn btn-primary"), content=my_content ) # Button with icon dropdown = Dropdown( parent=root, button=Div( icon_svg, Span("Options"), cls="flex items-center gap-2" ), content=my_content ) # Just an icon dropdown = Dropdown( parent=root, button=icon_svg, content=my_content ) ``` **Content:** Any FastHTML element to display in the dropdown panel. ```python # Simple list content = Ul( Li("Option 1"), Li("Option 2"), Li("Option 3"), cls="menu" ) # Complex content with sections content = Div( Div("User Actions", cls="font-bold p-2"), Hr(), Button("Edit Profile", cls="btn btn-ghost w-full"), Button("Settings", cls="btn btn-ghost w-full"), Hr(), Button("Logout", cls="btn btn-error w-full") ) ``` ### Positioning Options The Dropdown supports two positioning parameters: **`position`** - Vertical position relative to the button: - `"below"` (default): Dropdown appears below the button - `"above"`: Dropdown appears above the button **`align`** - Horizontal alignment relative to the button: - `"left"` (default): Dropdown aligns to the left edge of the button - `"right"`: Dropdown aligns to the right edge of the button - `"center"`: Dropdown is centered relative to the button ```python # Default: below + left dropdown = Dropdown(parent=root, button=btn, content=menu) # Above the button, aligned right dropdown = Dropdown(parent=root, button=btn, content=menu, position="above", align="right") # Below the button, centered dropdown = Dropdown(parent=root, button=btn, content=menu, position="below", align="center") ``` **Visual examples of all combinations:** ``` position="below", align="left" position="below", align="center" position="below", align="right" ┌────────┐ ┌────────┐ ┌────────┐ │ Button │ │ Button │ │ Button │ ├────────┴────┐ ┌────┴────────┴────┐ ┌────────────┴────────┤ │ Content │ │ Content │ │ Content │ └─────────────┘ └──────────────────┘ └─────────────────────┘ position="above", align="left" position="above", align="center" position="above", align="right" ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │ Content │ │ Content │ │ Content │ ├────────┬────┘ └────┬────────┬────┘ └────────────┬────────┤ │ Button │ │ Button │ │ Button │ └────────┘ └────────┘ └────────┘ ``` ## Advanced Features ### Automatic Close Behavior The Dropdown automatically closes in two scenarios: **Click outside:** When the user clicks anywhere outside the dropdown, it closes automatically. This is handled by the Mouse component listening for global click events. **ESC key:** When the user presses the ESC key, the dropdown closes. This is handled by the Keyboard component. ```python # Both behaviors are enabled by default - no configuration needed dropdown = Dropdown(parent=root, button=btn, content=menu) ``` **How it works internally:** - The `Mouse` component detects clicks and sends `is_inside` and `is_button` parameters - If `is_button` is true, the dropdown toggles - If `is_inside` is false (clicked outside), the dropdown closes - The `Keyboard` component listens for ESC and triggers the close command ### Programmatic Control You can control the dropdown programmatically using its methods and commands: ```python # Toggle the dropdown state dropdown.toggle() # Close the dropdown dropdown.close() # Access commands for use with other controls close_cmd = dropdown.commands.close() click_cmd = dropdown.commands.click() ``` **Using commands with buttons:** ```python from myfasthtml.controls.helpers import mk # Create a button that closes the dropdown close_button = mk.button("Close", command=dropdown.commands.close()) # Add it to the dropdown content dropdown = Dropdown( parent=root, button=Button("Menu"), content=Div( Ul(Li("Option 1"), Li("Option 2")), close_button ) ) ``` ### CSS Customization The Dropdown uses CSS classes that you can customize: | Class | Element | |-----------------------|---------------------------------------| | `mf-dropdown-wrapper` | Container with relative positioning | | `mf-dropdown-btn` | Button wrapper | | `mf-dropdown` | Dropdown content panel | | `mf-dropdown-below` | Applied when position="below" | | `mf-dropdown-above` | Applied when position="above" | | `mf-dropdown-left` | Applied when align="left" | | `mf-dropdown-right` | Applied when align="right" | | `mf-dropdown-center` | Applied when align="center" | | `is-visible` | Applied when dropdown is open | **Example customization:** ```css /* Change dropdown background and border */ .mf-dropdown { background-color: #1f2937; border: 1px solid #374151; border-radius: 0.5rem; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); } /* Add animation */ .mf-dropdown { opacity: 0; transform: translateY(-10px); transition: opacity 0.2s ease, transform 0.2s ease; } .mf-dropdown.is-visible { opacity: 1; transform: translateY(0); } /* Style for above position */ .mf-dropdown-above { transform: translateY(10px); } .mf-dropdown-above.is-visible { transform: translateY(0); } ``` ## Examples ### Example 1: Navigation Menu A simple navigation dropdown menu: ```python from fasthtml.common import * from myfasthtml.controls.Dropdown import Dropdown dropdown = Dropdown( parent=root, button=Button("Navigation", cls="btn btn-ghost"), content=Ul( Li(A("Dashboard", href="/dashboard", cls="block p-2 hover:bg-base-200")), Li(A("Projects", href="/projects", cls="block p-2 hover:bg-base-200")), Li(A("Tasks", href="/tasks", cls="block p-2 hover:bg-base-200")), Li(A("Reports", href="/reports", cls="block p-2 hover:bg-base-200")), cls="menu p-2" ) ) return dropdown ``` ### Example 2: User Account Menu A user menu aligned to the right, typically placed in a header: ```python from fasthtml.common import * from myfasthtml.controls.Dropdown import Dropdown # User avatar button user_button = Div( Img(src="/avatar.png", cls="w-8 h-8 rounded-full"), Span("John Doe", cls="ml-2"), cls="flex items-center gap-2 cursor-pointer" ) # Account menu content account_menu = Div( Div( Div("John Doe", cls="font-bold"), Div("john@example.com", cls="text-sm opacity-60"), cls="p-3 border-b" ), Ul( Li(A("Profile", href="/profile", cls="block p-2 hover:bg-base-200")), Li(A("Settings", href="/settings", cls="block p-2 hover:bg-base-200")), Li(A("Billing", href="/billing", cls="block p-2 hover:bg-base-200")), cls="menu p-2" ), Div( A("Sign out", href="/logout", cls="block p-2 text-error hover:bg-base-200"), cls="border-t" ), cls="w-56" ) # Align right so it doesn't overflow the viewport dropdown = Dropdown( parent=root, button=user_button, content=account_menu, align="right" ) return dropdown ``` ### Example 3: Action Menu Above Button A dropdown that opens above the trigger, useful when the button is at the bottom of the screen: ```python from fasthtml.common import * from myfasthtml.controls.Dropdown import Dropdown # Action button with icon action_button = Button( Span("+", cls="text-xl"), cls="btn btn-circle btn-primary" ) # Quick actions menu actions_menu = Div( Button("New Document", cls="btn btn-ghost btn-sm w-full justify-start"), Button("Upload File", cls="btn btn-ghost btn-sm w-full justify-start"), Button("Create Folder", cls="btn btn-ghost btn-sm w-full justify-start"), Button("Import Data", cls="btn btn-ghost btn-sm w-full justify-start"), cls="flex flex-col p-2 w-40" ) # Open above and center-aligned dropdown = Dropdown( parent=root, button=action_button, content=actions_menu, position="above", align="center" ) return dropdown ``` ### Example 4: Dropdown with Commands A dropdown containing action buttons that execute commands: ```python from fasthtml.common import * from myfasthtml.controls.Dropdown import Dropdown from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command # Define actions def edit_item(): return "Editing..." def delete_item(): return "Deleted!" def share_item(): return "Shared!" # Create commands edit_cmd = Command("edit", "Edit item", edit_item) delete_cmd = Command("delete", "Delete item", delete_item) share_cmd = Command("share", "Share item", share_item) # Build menu with command buttons actions_menu = Div( mk.button("Edit", command=edit_cmd, cls="btn btn-ghost btn-sm w-full justify-start"), mk.button("Share", command=share_cmd, cls="btn btn-ghost btn-sm w-full justify-start"), Hr(cls="my-1"), mk.button("Delete", command=delete_cmd, cls="btn btn-ghost btn-sm w-full justify-start text-error"), cls="flex flex-col p-2" ) dropdown = Dropdown( parent=root, button=Button("Actions", cls="btn btn-sm"), content=actions_menu ) return dropdown ``` --- ## Developer Reference This section contains technical details for developers working on the Dropdown component itself. ### State The Dropdown component maintains its state via `DropdownState`: | Name | Type | Description | Default | |----------|---------|------------------------------|---------| | `opened` | boolean | Whether dropdown is open | `False` | ### Commands Available commands for programmatic control: | Name | Description | |-----------|-------------------------------------------------| | `close()` | Closes the dropdown | | `click()` | Handles click events (toggle or close behavior) | **Command details:** - `close()`: Sets `opened` to `False` and returns updated content - `click()`: Receives `combination`, `is_inside`, and `is_button` parameters - If `is_button` is `True`: toggles the dropdown - If `is_inside` is `False`: closes the dropdown ### Public Methods | Method | Description | Returns | |------------|----------------------------|----------------------| | `toggle()` | Toggles open/closed state | Content tuple | | `close()` | Closes the dropdown | Content tuple | | `render()` | Renders complete component | `Div` | ### Constructor Parameters | Parameter | Type | Description | Default | |------------|-------------|------------------------------------|-----------| | `parent` | Instance | Parent instance (required) | - | | `content` | Any | Content to display in dropdown | `None` | | `button` | Any | Trigger element | `None` | | `_id` | str | Custom ID for the instance | `None` | | `position` | str | Vertical position: "below"/"above" | `"below"` | | `align` | str | Horizontal align: "left"/"right"/"center" | `"left"` | ### High Level Hierarchical Structure ``` Div(id="{id}") ├── Div(cls="mf-dropdown-wrapper") │ ├── Div(cls="mf-dropdown-btn") │ │ └── [Button content] │ └── Div(id="{id}-content", cls="mf-dropdown mf-dropdown-{position} mf-dropdown-{align} [is-visible]") │ └── [Dropdown content] ├── Keyboard(id="{id}-keyboard") │ └── ESC → close command └── Mouse(id="{id}-mouse") └── click → click command ``` ### Element IDs | Name | Description | |------------------|--------------------------------| | `{id}` | Root dropdown container | | `{id}-content` | Dropdown content panel | | `{id}-keyboard` | Keyboard handler component | | `{id}-mouse` | Mouse handler component | **Note:** `{id}` is the Dropdown instance ID (auto-generated or custom `_id`). ### Internal Methods | Method | Description | |-----------------|------------------------------------------| | `_mk_content()` | Renders the dropdown content panel | | `on_click()` | Handles click events from Mouse component | **Method details:** - `_mk_content()`: - Builds CSS classes based on `position` and `align` - Adds `is-visible` class when `opened` is `True` - Returns a tuple containing the content `Div` - `on_click(combination, is_inside, is_button)`: - Called by Mouse component on click events - `is_button`: `True` if click was on the button - `is_inside`: `True` if click was inside the dropdown - Returns updated content for HTMX swap