Select
Component

Select

A custom dropdown select component with full keyboard navigation and accessibility support. Supports strings, enums, and complex objects.

Demo

Selected: None

Features

  • Generic type support (string, enums, objects)
  • Full keyboard navigation with typeahead
  • Grouped items with labels
  • Flexible positioning with collision detection
  • EditForm integration with validation
  • Controlled and uncontrolled modes
  • WCAG compliant with proper ARIA attributes

Installation

bash
dotnet add package SummitUI

Anatomy

Import the components and structure them as follows:

razor
<SelectRoot TValue="string">
    <SelectTrigger TValue="string">
        <SelectValue TValue="string" Placeholder="Select an option..." />
    </SelectTrigger>
    <SelectPortal TValue="string">
        <SelectContent TValue="string">
            <SelectViewport TValue="string">
                <SelectItem TValue="string" Value="@("option-1")">
                    <SelectItemText>Option 1</SelectItemText>
                </SelectItem>
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

Sub-components

SelectRoot<TValue>

Generic root container managing select state.

SelectTrigger<TValue>

Button that opens the dropdown (combobox role).

SelectValue<TValue>

Displays selected value or placeholder text.

SelectPortal<TValue>

Renders content in fixed-position container.

SelectContent<TValue>

Floating listbox panel with positioning.

SelectViewport<TValue>

Scrollable container for items.

SelectItem<TValue>

Selectable option with option role.

SelectItemText

Text content wrapper for items.

SelectGroup<TValue>

Groups related items together.

SelectGroupLabel

Label for a group of items.

API Reference

SelectRoot<TValue>

The root component that manages select state and provides cascading context.

Property Type Default Description
Value TValue? null Controlled selected value
DefaultValue TValue? null Default value (uncontrolled)
ValueChanged EventCallback<TValue?> - Value change callback
ValueExpression Expression<Func<TValue?>>? null For EditForm validation
OnValueChange EventCallback<TValue?> - Alternative value change callback
Open bool? null Controlled open state
DefaultOpen bool false Default open state
OpenChanged EventCallback<bool> - Open state change callback
Disabled bool false Disable entire select
Required bool false For form validation
Invalid bool false For error styling
Name string? null Form field name for hidden input

SelectTrigger<TValue>

Button that opens the dropdown list.

Property Type Default Description
As string "button" HTML element to render
AriaLabel string? null Direct aria-label
AriaLabelledBy string? null ID of external label

SelectValue<TValue>

Displays the selected value or placeholder.

Property Type Default Description
Placeholder string? null Placeholder when no value selected
ChildContent RenderFragment? null Custom content

SelectContent<TValue>

Floating listbox panel with positioning options.

Property Type Default Description
As string "div" HTML element to render
Side Side Bottom Placement side
SideOffset int 4 Offset from trigger (px)
Align Align Start Alignment along side axis
AlignOffset int 0 Alignment offset (px)
AvoidCollisions bool true Avoid viewport boundaries
CollisionPadding int 8 Viewport padding (px)
EscapeKeyBehavior EscapeKeyBehavior Close Escape key behavior
OutsideClickBehavior OutsideClickBehavior Close Outside click behavior
OnInteractOutside EventCallback - Outside click callback
OnEscapeKeyDown EventCallback - Escape key callback

SelectItem<TValue>

Selectable option within the dropdown.

Property Type Default Description
Valuerequired TValue - Value of this item
Key string? null Optional string key for JS interop
Label string? null Label for typeahead and display
Disabled bool false Disable item
OnSelect EventCallback - Selection callback

Enums

Side

csharp
public enum Side
{
    Top,
    Right,
    Bottom,
    Left
}

Align

csharp
public enum Align
{
    Start,
    Center,
    End
}

EscapeKeyBehavior

csharp
public enum EscapeKeyBehavior
{
    Close,
    Ignore
}

OutsideClickBehavior

csharp
public enum OutsideClickBehavior
{
    Close,
    Ignore
}

Examples

Basic String Select

razor
@code {
    private string? selectedFruit;
}

<SelectRoot TValue="string" @bind-Value="selectedFruit">
    <SelectTrigger TValue="string" class="select-trigger">
        <SelectValue TValue="string" Placeholder="Select a fruit..." />
        <span class="select-icon">▼</span>
    </SelectTrigger>
    <SelectPortal TValue="string">
        <SelectContent TValue="string" class="select-content" SideOffset="4">
            <SelectViewport TValue="string" class="select-viewport">
                <SelectItem TValue="string" Value="@("apple")" Label="Apple">
                    <SelectItemText>Apple</SelectItemText>
                </SelectItem>
                <SelectItem TValue="string" Value="@("banana")" Label="Banana">
                    <SelectItemText>Banana</SelectItemText>
                </SelectItem>
                <SelectItem TValue="string" Value="@("cherry")" Label="Cherry">
                    <SelectItemText>Cherry</SelectItemText>
                </SelectItem>
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

<p>Selected: @(selectedFruit ?? "None")</p>

With Enum Values

Use strongly-typed enum values.

razor
@code {
    public enum Priority { Low, Medium, High, Critical }
    private Priority selectedPriority = Priority.Medium;
}

<SelectRoot TValue="Priority" @bind-Value="selectedPriority">
    <SelectTrigger TValue="Priority" class="select-trigger">
        <SelectValue TValue="Priority" Placeholder="Select priority..." />
    </SelectTrigger>
    <SelectPortal TValue="Priority">
        <SelectContent TValue="Priority" class="select-content">
            <SelectViewport TValue="Priority">
                <SelectItem TValue="Priority" Value="Priority.Low" Label="Low">
                    <SelectItemText>Low Priority</SelectItemText>
                </SelectItem>
                <SelectItem TValue="Priority" Value="Priority.Medium" Label="Medium">
                    <SelectItemText>Medium Priority</SelectItemText>
                </SelectItem>
                <SelectItem TValue="Priority" Value="Priority.High" Label="High">
                    <SelectItemText>High Priority</SelectItemText>
                </SelectItem>
                <SelectItem TValue="Priority" Value="Priority.Critical" Label="Critical">
                    <SelectItemText>Critical Priority</SelectItemText>
                </SelectItem>
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

With Complex Objects

Use custom objects as values.

razor
@code {
    public record Country(string Code, string Name);
    
    private List<Country> countries = new()
    {
        new("US", "United States"),
        new("UK", "United Kingdom"),
        new("CA", "Canada"),
        new("AU", "Australia")
    };
    
    private Country? selectedCountry;
}

<SelectRoot TValue="Country" @bind-Value="selectedCountry">
    <SelectTrigger TValue="Country" class="select-trigger">
        <SelectValue TValue="Country" Placeholder="Select a country..." />
    </SelectTrigger>
    <SelectPortal TValue="Country">
        <SelectContent TValue="Country" class="select-content">
            <SelectViewport TValue="Country">
                @foreach (var country in countries)
                {
                    <SelectItem TValue="Country" 
                                Value="country" 
                                Key="@country.Code" 
                                Label="@country.Name">
                        <SelectItemText>@country.Name (@country.Code)</SelectItemText>
                    </SelectItem>
                }
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

Grouped Items

Organize items into logical groups.

razor
<SelectRoot TValue="string" @bind-Value="selectedFood">
    <SelectTrigger TValue="string" class="select-trigger">
        <SelectValue TValue="string" Placeholder="Select food..." />
    </SelectTrigger>
    <SelectPortal TValue="string">
        <SelectContent TValue="string" class="select-content">
            <SelectViewport TValue="string">
                <SelectGroup TValue="string">
                    <SelectGroupLabel class="select-group-label">Fruits</SelectGroupLabel>
                    <SelectItem TValue="string" Value="@("apple")" Label="Apple">
                        <SelectItemText>Apple</SelectItemText>
                    </SelectItem>
                    <SelectItem TValue="string" Value="@("banana")" Label="Banana">
                        <SelectItemText>Banana</SelectItemText>
                    </SelectItem>
                </SelectGroup>
                <SelectGroup TValue="string">
                    <SelectGroupLabel class="select-group-label">Vegetables</SelectGroupLabel>
                    <SelectItem TValue="string" Value="@("carrot")" Label="Carrot">
                        <SelectItemText>Carrot</SelectItemText>
                    </SelectItem>
                    <SelectItem TValue="string" Value="@("broccoli")" Label="Broccoli">
                        <SelectItemText>Broccoli</SelectItemText>
                    </SelectItem>
                </SelectGroup>
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

EditForm Integration

Use with Blazor EditForm for validation.

razor
@code {
    public class FormModel
    {
        [Required(ErrorMessage = "Category is required")]
        public string? Category { get; set; }
    }
    
    private FormModel formModel = new();
    private EditContext? editContext;
    
    protected override void OnInitialized()
    {
        editContext = new EditContext(formModel);
    }
    
    private void HandleSubmit()
    {
        if (editContext!.Validate())
        {
            // Process form
        }
    }
}

<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    
    <div class="form-field">
        <label for="category">Category</label>
        <SelectRoot TValue="string" 
                    @bind-Value="formModel.Category" 
                    Name="category" 
                    Required="true">
            <SelectTrigger TValue="string" class="select-trigger" id="category">
                <SelectValue TValue="string" Placeholder="Select category..." />
            </SelectTrigger>
            <SelectPortal TValue="string">
                <SelectContent TValue="string" class="select-content">
                    <SelectViewport TValue="string">
                        <SelectItem TValue="string" Value="@("electronics")" Label="Electronics">
                            <SelectItemText>Electronics</SelectItemText>
                        </SelectItem>
                        <SelectItem TValue="string" Value="@("clothing")" Label="Clothing">
                            <SelectItemText>Clothing</SelectItemText>
                        </SelectItem>
                        <SelectItem TValue="string" Value="@("books")" Label="Books">
                            <SelectItemText>Books</SelectItemText>
                        </SelectItem>
                    </SelectViewport>
                </SelectContent>
            </SelectPortal>
        </SelectRoot>
        <ValidationMessage For="@(() => formModel.Category)" />
    </div>
    
    <button type="submit">Submit</button>
</EditForm>

Disabled State

Disable entire select or individual items.

razor
@* Disabled entire select *@
<SelectRoot TValue="string" Disabled="true">
    <SelectTrigger TValue="string" class="select-trigger">
        <SelectValue TValue="string" Placeholder="Disabled..." />
    </SelectTrigger>
    ...
</SelectRoot>

@* Disabled individual items *@
<SelectRoot TValue="string" @bind-Value="selectedValue">
    <SelectTrigger TValue="string" class="select-trigger">
        <SelectValue TValue="string" Placeholder="Select..." />
    </SelectTrigger>
    <SelectPortal TValue="string">
        <SelectContent TValue="string" class="select-content">
            <SelectViewport TValue="string">
                <SelectItem TValue="string" Value="@("option1")" Label="Option 1">
                    <SelectItemText>Option 1</SelectItemText>
                </SelectItem>
                <SelectItem TValue="string" Value="@("option2")" Label="Option 2" Disabled="true">
                    <SelectItemText>Option 2 (Disabled)</SelectItemText>
                </SelectItem>
                <SelectItem TValue="string" Value="@("option3")" Label="Option 3">
                    <SelectItemText>Option 3</SelectItemText>
                </SelectItem>
            </SelectViewport>
        </SelectContent>
    </SelectPortal>
</SelectRoot>

Styling

Data Attributes

Attribute Values Description
data-state "open" | "closed" Dropdown open state (on trigger/content)
data-state "checked" | "unchecked" Item selection state
data-highlighted Present when focused Item is keyboard-focused
data-disabled Present when disabled Item or trigger is disabled
data-placeholder Present when no value Showing placeholder text
data-invalid Present when invalid For form validation errors

CSS Example

css
.select-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 200px;
    padding: 8px 12px;
    background: white;
    border: 1px solid #ccc;
    border-radius: 6px;
    cursor: pointer;
}

.select-trigger[data-state="open"] {
    border-color: #0066cc;
    box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}

.select-trigger[data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
}

.select-trigger[data-placeholder] {
    color: #999;
}

.select-content {
    background: white;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    overflow: hidden;
}

.select-viewport {
    padding: 4px;
    max-height: 300px;
    overflow-y: auto;
}

.select-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-radius: 4px;
    cursor: pointer;
}

.select-item[data-highlighted] {
    background: rgb(var(--su-accent));
    color: rgb(var(--su-accent-foreground));
}

.select-item[data-state="checked"] {
    background: #e6f0ff;
}

.select-item[data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
}

.select-group-label {
    padding: 8px 12px;
    font-size: 12px;
    font-weight: 600;
    color: #666;
    text-transform: uppercase;
}

Accessibility

Keyboard Navigation

Key Action
Enter / Space Open dropdown or select highlighted item
ArrowDown Open dropdown or move to next item
ArrowUp Move to previous item
Home Move to first item
End Move to last item
Escape Close dropdown
A-Z / a-z Typeahead - jump to matching item

Typeahead

Typing characters while the dropdown is open will jump to items that match the typed text. The search buffer resets after a short delay.

ARIA Attributes

  • SelectTrigger: Has role="combobox", aria-haspopup="listbox", and aria-expanded
  • SelectContent: Has role="listbox"
  • SelectItem: Has role="option" with aria-selected
  • SelectGroup: Has role="group" with aria-labelledby
  • Disabled items: Have aria-disabled
An unhandled error has occurred. Reload X