# DropdownMenu A button that, when clicked, opens a small floating menu of links. Like a folder label on a filing cabinet — pull it and a list of options drops down. Closes automatically when you click elsewhere or press Escape. --- ## Quick example ```csharp new DropdownMenu( trigger: new Button("Account", variant: "outline"), items: new[] { ("Profile", "/profile", false), ("Settings", "/settings", false), ("", "", true), // separator line ("Sign out", "/logout", false), }) ``` --- ## All the options ```csharp public DropdownMenu( IHtmxComponent trigger, IEnumerable<(string Label, string Href, bool IsSeparator)> items, string position = "right") ``` | Parameter | What it does | |---|---| | `trigger` | The visible button that opens the menu. Any `IHtmxComponent` — usually a `Button`. | | `items` | The list of menu items. Each is a `(Label, Href, IsSeparator)` tuple. | | `position` | `"right"` aligns the menu to the right edge of the trigger (default). `"left"` aligns it to the left. | **Item tuple fields:** | Field | What it does | |---|---| | `Label` | The text shown in the menu. | | `Href` | The URL to navigate to when clicked. | | `IsSeparator` | Set to `true` to render a divider line instead of a link. `Label` and `Href` are ignored. | --- ## Real-world examples ### User account menu in the header ```csharp new DropdownMenu( trigger: new Button("My account", variant: "ghost"), items: new[] { ("Profile", "/profile", false), ("Billing", "/billing", false), ("", "", true), ("Sign out", "/logout", false), }) ``` ### Row action menu in a table (three-dot icon button) ```csharp new DropdownMenu( trigger: new Button("⋯", size: "icon", variant: "ghost"), items: new[] { ("Edit", $"/items/{item.Id}/edit", false), ("Delete", $"/items/{item.Id}/delete", false), }, position: "left") // avoid overflow near the right edge of the screen ``` ### Left-aligned menu (near right side of viewport) Use `position: "left"` when the trigger is close to the right edge of the screen to prevent the menu from clipping off-screen: ```csharp new DropdownMenu( trigger: new Button("Actions"), items: actions, position: "left") ``` --- ## How it works The menu panel is always present in the HTML but hidden with a `hidden` class. When the trigger is clicked, JavaScript toggles the `hidden` class to show it. Clicking anything outside — or pressing Escape — adds `hidden` back. Because the click listener is attached to `document`, dropdown menus that are HTMX-swapped in work automatically. All items are rendered as `` links. If you need an action that POSTs data (like a delete), the cleanest approach is to route it through a confirmation Dialog. --- ## Complete page example **`Templates/UserHeaderPage.htmx`** ```html
MyApp
Dashboard $$UserMenu$$
$$Body$$
``` **`Templates/UserHeaderPage.htmx.cs`** ```csharp namespace Htmx.ApiDemo.Templates; public sealed class UserHeaderPage : UserHeaderPageBase { private readonly IHtmxComponent _userMenu; private readonly IHtmxComponent _body; public UserHeaderPage(AppUser user, IHtmxComponent body) { _body = body; var trigger = new Components.Avatar( fallback: GetInitials(user.DisplayName), size: "sm"); _userMenu = new Components.DropdownMenu( trigger: trigger, items: new[] { ("Profile", "/profile", false), ("Settings", "/settings", false), ("", "", true), // separator ("Sign out", "/logout", false), }); } private static string GetInitials(string? name) { if (string.IsNullOrWhiteSpace(name)) return "?"; var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries); return parts.Length >= 2 ? $"{parts[0][0]}{parts[^1][0]}" : name[..1].ToUpperInvariant(); } protected override void RenderUserMenu(HtmxRenderContext ctx) => _userMenu.Render(ctx.Next()); protected override void RenderBody(HtmxRenderContext ctx) => _body.Render(ctx.Next()); } ``` **GET handler** ```csharp [Handler] [MapGet("/home")] public static partial class GetHomeHandler { public record Query(); private static async Task HandleAsync( Query _, HttpContext ctx, MongoDbService db, CancellationToken ct) { var email = ctx.User.FindFirst(ClaimTypes.Email)?.Value ?? ""; var user = await db.FindByNormalizedEmailAsync(email.ToUpperInvariant(), ct) ?? new AppUser { Email = email }; // The inner body can be any page component var innerBody = new WelcomePage(user); var page = new UserHeaderPage(user, innerBody); return await ctx.WriteHtmxPage(page, title: "Home"); } } ```