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
dotnet add package SummitUIAnatomy
Import the components and structure them as follows:
<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
public enum Side
{
Top,
Right,
Bottom,
Left
}Align
public enum Align
{
Start,
Center,
End
}EscapeKeyBehavior
public enum EscapeKeyBehavior
{
Close,
Ignore
}OutsideClickBehavior
public enum OutsideClickBehavior
{
Close,
Ignore
}Examples
Basic String Select
@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.
@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.
@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.
<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.
@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.
@* 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
.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", andaria-expanded - SelectContent:
Has
role="listbox" - SelectItem:
Has
role="option"witharia-selected - SelectGroup:
Has
role="group"witharia-labelledby - Disabled items:
Have
aria-disabled