M
Marai.UI
v1.0.0-alpha.7

Select

A native <select> wrapper with built-in sizing, semantic token styling, and full Tailwind override support. Supports two-way binding, placeholder, disabled options, form validation, and arbitrary HTML attribute passthrough.

Overview

MSelect renders a styled native <select> that matches MInput in sizing and visual language out of the box. Use Size to control height, padding, and font size. Pass Class for safe Tailwind overrides — user-supplied classes always win conflicts. The component handles single-value binding, placeholder option rendering, disabled state, disabled per-option, typed option data, and arbitrary HTML attribute forwarding.

Usage

Add the namespace import and include the stylesheet reference if not already done.

razor
@using Marai.UI.Components.MSelect

Basic Usage

Preview
razor
<div class="relative max-w-xs">
    <MSelect Items="_fruits"
             Placeholder="Pick a fruit"
             Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
        <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
            <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
        </svg>
    </div>
</div>

@code {
    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry"),
        new("mango",  "Mango"),
    ];
}

Examples

Placeholder

Set Placeholder to render an initial disabled empty option prompting the user to pick a value.

Preview
razor
<div class="relative max-w-xs">
    <MSelect Items="_fruits"
             Placeholder="Select a fruit..."
             Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
        <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
            <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
        </svg>
    </div>
</div>

@code {
    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry"),
        new("mango",  "Mango"),
    ];
}

Default Selected

Supply a Value to pre-select an option on first render.

Preview
razor
<div class="relative max-w-xs">
    <MSelect Items="_fruits"
             Value="banana"
             Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
        <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
            <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
        </svg>
    </div>
</div>

@code {
    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry"),
        new("mango",  "Mango"),
    ];
}

Two-Way Binding

Use @bind-Value to keep a field in sync with the selected option.

Preview

Selected: none

razor
<div class="flex flex-col gap-3 max-w-xs">
    <div class="relative">
        <MSelect Items="_fruits"
                 @bind-Value="_selected"
                 Placeholder="Choose a fruit"
                 Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
        <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
            <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
            </svg>
        </div>
    </div>
    <p class="text-sm text-muted-foreground">Selected: @(_selected ?? "none")</p>
</div>

@code {
    private string? _selected;

    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry"),
        new("mango",  "Mango"),
    ];
}

Disabled

Pass Disabled="true" to set the native disabled attribute. Apply disabled visual styles (e.g. disabled:cursor-not-allowed disabled:opacity-50) in your Class string.

Preview
razor
<div class="relative max-w-xs">
    <MSelect Items="_fruits"
             Value="apple"
             Disabled="true"
             Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500 disabled:cursor-not-allowed disabled:opacity-50" />
    <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3 opacity-40">
        <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
            <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
        </svg>
    </div>
</div>

@code {
    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry"),
        new("mango",  "Mango"),
    ];
}

Disabled Option

Set Disabled = true on an individual MSelectOption to prevent selecting that specific item.

Preview
razor
<div class="relative max-w-xs">
    <MSelect Items="_fruits"
             Placeholder="Choose a fruit"
             Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
        <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
            <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
        </svg>
    </div>
</div>

@code {
    private static readonly MSelectOption[] _fruits =
    [
        new("apple",  "Apple"),
        new("banana", "Banana"),
        new("cherry", "Cherry (unavailable)", Disabled: true),
        new("mango",  "Mango"),
    ];
}

In a Form

Use @bind-Value inside an EditForm for model binding and validation support.

Note

Supply a ValueExpression when you need <ValidationMessage> to identify the field. With @bind-Value this is handled automatically by the Blazor compiler.

razor
<EditForm Model="_model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    <div class="flex flex-col gap-2 max-w-xs">
        <MLabel For="role">Role</MLabel>
        <div class="relative">
            <MSelect id="role"
                     Items="_roles"
                     @bind-Value="_model.Role"
                     Placeholder="Select a role"
                     Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500 disabled:cursor-not-allowed disabled:opacity-50" />
            <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
                <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                    <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
                </svg>
            </div>
        </div>
        <ValidationMessage For="() => _model.Role" />
    </div>
    <MButton Type="submit" Class="mt-4">Submit</MButton>
</EditForm>

@code {
    private readonly MyModel _model = new();

    private static readonly MSelectOption[] _roles =
    [
        new("admin",  "Admin"),
        new("editor", "Editor"),
        new("viewer", "Viewer"),
    ];

    private void HandleSubmit() { /* ... */ }
}

Standard HTML Attributes

MSelect supports all standard HTML select attributes via AdditionalAttributes. Pass any attribute directly — id, name, aria-label, aria-describedby — and it will be forwarded to the underlying <select>.

Preview

Your role determines your access level.

razor
<div class="flex flex-col gap-2 max-w-xs">
    <MLabel For="role-select">Role</MLabel>
    <div class="relative">
        <MSelect id="role-select"
                 name="role"
                 aria-label="Select your role"
                 aria-describedby="role-hint"
                 Items="_roles"
                 Placeholder="Choose a role"
                 Class="w-full rounded-md border border-slate-300 bg-white pl-3 pr-10 py-2 text-sm text-slate-900 appearance-none focus:outline-none focus:ring-2 focus:ring-sky-500" />
        <div class="pointer-events-none absolute top-1/2 -translate-y-1/2 right-3">
            <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
            </svg>
        </div>
    </div>
    <p id="role-hint" class="text-sm text-muted-foreground">Your role determines your access level.</p>
</div>

@code {
    private static readonly MSelectOption[] _roles =
    [
        new("admin",  "Admin"),
        new("editor", "Editor"),
        new("viewer", "Viewer"),
    ];
}
Customization Examples
  • Width: Class="w-48" or Class="max-w-xs"
  • Font: Class="font-medium" or Class="text-base"
  • Accessibility: aria-label="...", aria-describedby="..."
  • Forms: name="..." for form submission grouping
  • Testing: data-testid="...", id="..."

Accessibility

  • Renders a native <select> — fully keyboard accessible out of the box
  • Always associate a visible label using MLabel with matching id and For attributes
  • Use aria-label when a visible label is not present
  • Use aria-describedby to link help text or error messages to the select
  • Disabled options use the disabled attribute on the <option> element
  • Disabled select uses the native disabled attribute; apply visual disabled styling (cursor, opacity) via your Class string

API Reference

MSelect

Parameter Type Default Description
Value string? null The currently selected value. Use with @bind-Value for two-way binding.
ValueChanged EventCallback<string?> Callback invoked when the selection changes. Wired automatically by @bind-Value.
ValueExpression Expression<Func<string?>>? null Expression identifying the bound field for <ValidationMessage>. Set automatically by @bind-Value.
Placeholder string? null When set, renders a leading disabled empty option with this text as a prompt.
Items IEnumerable<MSelectOption>? null The collection of options to render in the select.
Size Size Size.Default Height, padding, and font-size preset (Sm, Default, Lg). Matches MInput sizing.
Disabled bool false Sets the native disabled attribute on the <select>. Default styling applies disabled:cursor-not-allowed disabled:opacity-50.
Class string? null Additional Tailwind classes applied to the native <select>. User-supplied classes win all conflicts.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes forwarded to the <select> element (e.g. id, name, aria-*, data-*).

MSelectOption

Property Type Default Description
Value string The value submitted when this option is selected.
Label string The display text shown in the option list.
Disabled bool false When true, the option is rendered but cannot be selected.
Best Practices
  • Always pair with MLabel using matching id and For for accessible association
  • Use Placeholder rather than a blank default option to guide users
  • Use @bind-Value for reactive state over manual Value + ValueChanged
  • Mark unavailable options with Disabled: true rather than removing them — this preserves context
  • Add name attribute when the select is inside a native HTML form
  • Add data-testid attributes to target selects in automated tests
  • Add appearance-none to your Class string if you want to suppress the browser-native arrow and supply your own