M
Marai.UI
v1.0.0-alpha.7

Dropdown

A composable dropdown with built-in open/close state, value selection, keyboard navigation, and full ARIA semantics — all in pure C#, no JavaScript required. Style every part with Tailwind utilities via the Class parameter.

Overview

The MDropdown family manages all behavior so you can focus on styling. MDropdownTrigger toggles open state; MDropdownContent renders the menu panel with keyboard handling; MDropdownItem handles selection, auto-close, and ARIA; MDropdownLabel and MDropdownSeparator provide structural markup. Apply Class to every part to compose the visual design you need.

Usage

Add the namespace import to use the component.

razor
@using Marai.UI.Components.MDropdown

Basic Usage

A value dropdown with explicit Tailwind classes on every part:

Preview
razor
<MDropdown @bind-Value="_selectedStatus" Class="relative inline-block w-56">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        Placeholder="Select status">
        <SelectedValueContent>
            @context
        </SelectedValueContent>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownItem Value="All"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            All
        </MDropdownItem>
        <MDropdownItem Value="Unreconciled"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Unreconciled
        </MDropdownItem>
        <MDropdownItem Value="Reconciled"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Reconciled
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _selectedStatus;
}

Examples

Use as an action menu. Items close the dropdown automatically after firing OnClick:

Preview
razor
<MDropdown @bind-Value="_lastAction" Class="relative inline-block w-48">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        Placeholder="Choose action">
        <SelectedValueContent>
            @context
        </SelectedValueContent>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownItem Value="Copy"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Copy
        </MDropdownItem>
        <MDropdownItem Value="Paste"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Paste
        </MDropdownItem>
        <MDropdownSeparator Class="my-1 border-t border-border" />
        <MDropdownItem Value="Delete"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Delete
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _lastAction;
}

Labels and Separators

Use MDropdownLabel for section headings and MDropdownSeparator to divide groups. Style both with Class:

Preview
razor
<MDropdown @bind-Value="_accountAction" Class="relative inline-block w-48">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        Placeholder="My Account">
        <SelectedValueContent>
            @context
        </SelectedValueContent>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownLabel Class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Account</MDropdownLabel>
        <MDropdownSeparator Class="my-1 border-t border-border" />
        <MDropdownItem Value="Profile"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Profile
        </MDropdownItem>
        <MDropdownItem Value="Billing"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Billing
        </MDropdownItem>
        <MDropdownItem Value="Settings"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Settings
        </MDropdownItem>
        <MDropdownSeparator Class="my-1 border-t border-border" />
        <MDropdownItem Value="Sign out"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Sign out
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _accountAction;
}

Disabled Items

Set Disabled="true" on MDropdownItem. The component adds aria-disabled="true" and data-disabled="true", allowing Tailwind's aria-disabled: modifier to style the item without extra conditional logic:

Preview
razor
<MDropdown @bind-Value="_fileAction" Class="relative inline-block w-48">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        Placeholder="File options">
        <SelectedValueContent>
            @context
        </SelectedValueContent>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownItem Value="Edit"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground aria-disabled:pointer-events-none aria-disabled:opacity-50">
            Edit
        </MDropdownItem>
        <MDropdownItem Value="Export" Disabled="true"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground aria-disabled:pointer-events-none aria-disabled:opacity-50">
            Export (unavailable)
        </MDropdownItem>
        <MDropdownSeparator Class="my-1 border-t border-border" />
        <MDropdownItem Value="Delete"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground aria-disabled:pointer-events-none aria-disabled:opacity-50">
            Delete
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _fileAction;
}

Custom Trigger Icon

Supply any icon or content via ChildContent on MDropdownTrigger. No built-in chevron is rendered:

Preview
razor
<MDropdown @bind-Value="_selectedOption" Class="relative inline-block w-56">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
        @if (!string.IsNullOrEmpty(_selectedOption))
        {
            <span>@_selectedOption</span>
        }
        else
        {
            <span class="text-muted-foreground">Select option</span>
        }
        <svg class="ml-2 h-4 w-4 opacity-60" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="m6 9 6 6 6-6"/>
        </svg>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownItem Value="Profile"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Profile
        </MDropdownItem>
        <MDropdownItem Value="Settings"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Settings
        </MDropdownItem>
        <MDropdownItem Value="Sign out"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Sign out
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _selectedOption;
}

Outside-Click Dismissal

Pass OverlayClass to MDropdownContent to render a click-capture layer behind the menu. Use fixed inset-0 to cover the full viewport. Without OverlayClass no overlay is rendered:

Preview
razor
<MDropdown @bind-Value="_action" Class="relative inline-block w-48">
    <MDropdownTrigger
        Class="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        Placeholder="Pick action">
        <SelectedValueContent>@context</SelectedValueContent>
    </MDropdownTrigger>
    <MDropdownContent
        Class="absolute left-0 top-full z-50 mt-1 w-full min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md"
        OverlayClass="fixed inset-0 z-40">
        <MDropdownItem Value="Edit"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Edit
        </MDropdownItem>
        <MDropdownItem Value="Archive"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Archive
        </MDropdownItem>
        <MDropdownItem Value="Delete"
            Class="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted data-selected:bg-primary data-selected:text-primary-foreground">
            Delete
        </MDropdownItem>
    </MDropdownContent>
</MDropdown>

@code {
    private string? _action;
}

API Reference

MDropdown

Parameter Type Default Description
ChildContent RenderFragment? null Slot content — typically MDropdownTrigger and MDropdownContent.
Value string? null The currently selected value. Use with @bind-Value for two-way binding.
ValueChanged EventCallback<string?> Callback invoked when the selected value changes.
Class string? null CSS classes applied to the root container element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through to the root element.

MDropdownTrigger

Parameter Type Default Description
ChildContent RenderFragment? null Full trigger content. When provided, overrides SelectedValueContent and Placeholder.
SelectedValueContent RenderFragment<string>? null Template rendered when a value is selected and ChildContent is null. Context is the selected value string.
Placeholder string? null Text rendered when no value is selected and no ChildContent is provided.
Class string? null CSS classes applied to the trigger button element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through to the button element.

MDropdownContent

Parameter Type Default Description
ChildContent RenderFragment? null Items, labels, and separators rendered inside the panel.
Class string? null CSS classes applied to the menu panel element.
OverlayClass string? null CSS classes applied to the outside-click capture layer. When null, no overlay is rendered and clicking outside does not close the menu.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through to the menu panel element.

MDropdownItem

Parameter Type Default Description
ChildContent RenderFragment? null Item label or content.
Value string? null The value set on the dropdown when this item is clicked.
Disabled bool false Prevents interaction. Sets disabled, aria-disabled="true", and data-disabled="true" on the element.
OnClick EventCallback Callback invoked when the item is clicked. The menu closes automatically after invocation.
Class string? null CSS classes applied to the item element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through to the item element.
Tip

MDropdownItem adds data-selected="true" and aria-selected="true" when its value matches the dropdown's selected value. Use Tailwind's data-[selected]:... or aria-selected:... modifiers to style selected state without conditional logic.

MDropdownLabel

Parameter Type Default Description
ChildContent RenderFragment? null Group heading text or content.
Class string? null CSS classes applied to the label element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through.

MDropdownSeparator

Parameter Type Default Description
Class string? null CSS classes applied to the separator element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through.

Keyboard Navigation

Key Context Action
ArrowDownTrigger focused, menu closedOpen menu and focus first enabled item.
ArrowUpTrigger focused, menu closedOpen menu and focus last enabled item.
Enter / SpaceTrigger focusedToggle menu open/closed (native button behavior).
ArrowDownMenu openMove focus to next enabled item (wraps).
ArrowUpMenu openMove focus to previous enabled item (wraps).
HomeMenu openMove focus to first enabled item.
EndMenu openMove focus to last enabled item.
Enter / SpaceItem focusedActivate item (native button behavior).
EscapeMenu openClose menu and return focus to trigger.
TabMenu openClose menu and move focus naturally.

Accessibility

  • MDropdownTrigger renders with aria-haspopup="menu", aria-expanded, and aria-controls pointing to the menu panel ID
  • MDropdownContent renders with role="menu" and a stable generated id
  • MDropdownItem renders with role="menuitem", tabindex="-1", and aria-disabled when disabled
  • Disabled items are skipped during keyboard navigation
  • Escape closes the menu and returns focus to the trigger automatically
  • ArrowDown / ArrowUp from the trigger open the menu and land focus on the first or last enabled item respectively