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