A reusable Blazor dropdown component can keep a menu trigger, item templates, selected styling, and selection callbacks in one place. The original .NET 8 sample in this guide builds that behavior from four generic Razor components and supports countries, strings, and enum values without duplicating the menu structure.

Important distinction: this is a custom action-style dropdown menu rendered with a button and div items. It is not a native HTML <select>, and it does not integrate with EditForm validation. For a typed form input, use the Blazor select component with binding and validation instead.

Version note: The repository, screenshots, and video were created with .NET 8 in June 2024. The original code remains unchanged below. This refresh rewrites the explanation and adds production, accessibility, and compatibility guidance; it does not claim that the historical project was rebuilt or tested on a newer framework.

Download or inspect the complete sample in the original Blazor dropdown component repository.

Table of Contents

How the Blazor Dropdown Component Works

The sample separates the dropdown into four responsibilities. DncDropdown<TItem> owns open state and the selection callback. DncDropdownButton<TItem> toggles the menu. DncDropdownMenu<TItem> conditionally renders the item container. DncDropdownItem<TItem, TValue> extracts a value, renders an optional template, and tells the parent which item was selected.

The parent component flows itself to descendants through CascadingValue. That avoids passing the same state and methods through every intermediate component. The design is useful when menu items need rich markup, but it also creates a tight relationship between the four components: the child components only work inside a matching DncDropdown<TItem>.

Component tree for the reusable Blazor dropdown
The original component tree shows the parent, button, menu, and generic items.

Create the Solution and Razor Class Library

The original solution contains a Razor class library named Dnc.Common.Razor and a Blazor Web App named Dnc.Dropdown.WebApp, both targeting net8.0. Create a Dropdown folder in the class library for the four reusable components, then reference that library from the web app.

Because opening the menu and selecting an item depend on @onclick, the component must run in an interactive render mode. If it renders but does not respond, verify the app’s render-mode configuration before changing the component code.

Build the Parent Dropdown Component

Create DncDropdown.razor. The following is the original implementation:

@typeparam TItem
<CascadingValue Value="@this">
    <div class="dropdown">
        @ChildContent
    </div>
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public EventCallback<TItem> OnSelected { get; set; }

    public bool Show { get; private set; }

    private object selectedValue;

    public object SelectedValue {
        get { return this.selectedValue; }
        set
        {
            this.selectedValue = value;
            StateHasChanged();
        }
    }

    public void Toggle()
    {
        Show = !Show;
        StateHasChanged();
    }

    public async Task HandleSelect(TItem item, object selectedValue)
    {
        this.SelectedValue = selectedValue;
        Show = false;
        StateHasChanged();
        await this.OnSelected.InvokeAsync(item);
    }
}

ChildContent receives the nested button and menu. OnSelected sends the complete selected item to the consuming page. Toggle changes visibility, while HandleSelect stores the extracted value, closes the menu, and awaits the callback.

The explicit StateHasChanged() calls are preserved. Blazor normally rerenders automatically after its event handlers, so several calls are redundant in this implementation. They are not proof of a bug, but a production refactor should remove unnecessary renders only after behavior tests are in place.

Add the Button and Menu Components

Create DncDropdownButton.razor. It receives the parent through a cascading parameter and calls Toggle when clicked.

@typeparam TItem

<button class="btn btn-primary" @onclick="HandleClick" type="button"
        id="dropdownMenuButton" data-toggle="dropdown"
        aria-haspopup="true" aria-expanded="false">
    @ChildContent
</button>

@code {
    [CascadingParameter]
    public DncDropdown<TItem> DncDropdown { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private void HandleClick()
    {
        this.DncDropdown.Toggle();
    }
}

Next create DncDropdownMenu.razor. It adds the Bootstrap show class only when the parent state is open.

@typeparam TItem

@if (DncDropdown.Show)
{
    <div class="dropdown-menu show" aria-labelledby="dropdownMenuButton">
        @ChildContent
    </div>
}

@code {
    [CascadingParameter]
    public DncDropdown<TItem> DncDropdown { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

The visibility is controlled by Blazor, not by Bootstrap JavaScript. The historical data-toggle attribute therefore does not control this menu. Production code should either remove unused Bootstrap behavior attributes or deliberately integrate the matching Bootstrap version.

DncDropdownItem accepts an item and an expression that selects its comparison value. A typed RenderFragment<TItem> lets each caller decide how the row should look.

@using System.Linq.Expressions
@typeparam TItem
@typeparam TValue

<div class="dropdown-item @CssSelected @isDisabled"
     data-value="@ItemValue" Item="@Item"
     @onclick="e=> DncDropdown.HandleSelect(Item, ItemValue)">
    @if (HasChild)
    {
        @ChildContent(Item)
    }
    else
    {
        @ItemValue
    }
</div>

@code {
    [CascadingParameter]
    public DncDropdown<TItem> DncDropdown { get; set; }

    [Parameter]
    public RenderFragment<TItem> ChildContent { get; set; }

    [Parameter]
    public TItem Item { get; set; }

    [Parameter]
    public Expression<Func<TItem, TValue>> Value { get; set; }

    [Parameter]
    public bool Selected { get; set; }

    [Parameter]
    public bool Disabled { get; set; }

    private bool HasChild => ChildContent != null;
    private string isDisabled => Disabled ? "disabled" : "";
    private object ItemValue => GetValue(Value);

    private string CssSelected
    {
        get
        {
            if (DncDropdown.SelectedValue == null && Selected)
            {
                return "selected";
            }
            else
            {
                return ItemValue?.ToString() ==
                       DncDropdown.SelectedValue?.ToString()
                    ? "selected" : "";
            }
        }
    }

    private object GetValue(Expression<Func<TItem, TValue>> expression)
    {
        var compiledExpression = expression.Compile();
        try
        {
            return compiledExpression(Item);
        }
        catch
        {
            return null;
        }
    }
}

The original CSS supplies selected and disabled appearances:

.selected {
    color: #fff;
    background-color: #0d6efd;
}

.disabled {
    pointer-events: none;
    opacity: 0.9;
}

.dropdown-item {
    width: 100%;
}

.dropdown-item:active {
    background-color: #80aded;
}

This works visually for the demonstration, but CSS does not create disabled semantics for a div. The limitations section explains what must change before using this pattern as an accessible production control.

Use the Dropdown in a Blazor Page

Reference the class library, import Dnc.Common.Razor.Dropdown, and then compose the four components. This preserved example renders countries and sends the selected Country object to the page:

<DncDropdown TItem="Country" OnSelected="@HandleSelectedLanguage">
    <DncDropdownButton TItem="Country">
        <i class="fas fa-language"></i> Language Settings
    </DncDropdownButton>
    <DncDropdownMenu TItem="Country">
        @foreach (var country in countries)
        {
            <DncDropdownItem Item="@country"
                             Value="(Country v) => v.Language">
                <div class="dnc-div">
                    @context.Name
                    <span class="dnc-span">@context.Language</span>
                </div>
            </DncDropdownItem>
        }
    </DncDropdownMenu>
</DncDropdown>

@code {
    private string selectedLanguageSetting = string.Empty;

    private readonly List<Country> countries = new()
    {
        new Country { Name = "USA", Language = "EN" },
        new Country { Name = "France", Language = "FR" },
        new Country { Name = "Egypt", Language = "AR" }
    };

    private void HandleSelectedLanguage(Country value)
    {
        selectedLanguageSetting = value?.Language;
    }

    public class Country
    {
        public string Name { get; set; }
        public string Language { get; set; }
    }
}

The repository also demonstrates a string menu with one preselected item and one disabled item, plus an enum menu for seasons. Those examples show that TItem is not limited to one model type.

Three reusable Blazor dropdown menus in the original sample
The original .NET 8 app demonstrates country, phone-setting, and season dropdowns.

Verify the Original Behavior

Run the original app and verify each behavior instead of checking only the final appearance:

  • Each button opens and closes only its own menu.
  • Selecting an enabled item closes the menu and invokes the correct callback once.
  • The chosen row receives the selected style.
  • The initially selected phone setting is highlighted before a new choice.
  • The disabled demonstration item does not respond to a pointer click.
  • Country, string, and enum items display the expected values.
The original .NET 8 demonstration recorded in June 2024.

Production and Accessibility Limitations

The sample explains component composition, but it is not a complete accessible menu or listbox. Decide what interaction you are building before extending it. A menu of actions, a listbox that selects a value, and a form select have different semantic and keyboard requirements.

  • Use semantic controls. Clickable div elements are not keyboard-operable by default. Menu items need appropriate roles, focusability, keyboard handling, and disabled state. A form value should normally use InputSelect or a custom InputBase<TValue> component.
  • Synchronize ARIA state. The original button hard-codes aria-expanded="false". It should reflect Show. The repeated dropdownMenuButton ID must also become unique for every instance.
  • Manage focus. Opening should move focus according to the chosen interaction pattern. Support arrow keys where appropriate, Escape to close, and a defined focus return to the trigger.
  • Handle outside interaction. The sample does not close when the user clicks elsewhere. Add outside-click behavior deliberately and test it with nested interactive content.
  • Keep values typed. object SelectedValue and ToString() comparison lose type safety and can mark different values as equal. Prefer a typed value and an equality comparer.
  • Avoid repeated expression compilation. Expression.Compile() can run repeatedly as properties are evaluated. A simple Func<TItem, TValue> or a cached compiled delegate is clearer unless the expression tree itself is required.
  • Do not hide failures. Catching every exception and returning null makes configuration errors hard to diagnose. Validate required parameters and fail with a useful message.
  • Protect asynchronous selections. If OnSelected starts slow work, consider a busy state, duplicate-click prevention, cancellation, and visible error handling.

These are not claims that the historical demo failed. They define the gap between a working visual sample and a reusable production component that supports keyboard users, assistive technology, multiple instances, and asynchronous application work.

Current Compatibility Status

The repository targets .NET 8, which remains supported until November 10, 2026. .NET 10 is the current LTS release, but this article does not present an unverified upgrade. If you move the project to a newer target framework, rebuild it, run the interaction checklist above, verify the render mode, and retest any Bootstrap or icon-library dependencies.

Conclusion

The original Blazor dropdown component is a compact example of generic Razor components, cascading parent state, templated content, and EventCallback<TItem>. It is suitable for learning how a component family collaborates and for controlled menu-style interfaces. Before production use, choose the correct semantics, implement keyboard and focus behavior, use unique IDs, keep selection typed, and test asynchronous callbacks.

Official References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO