Rask
Rask
v0.23.1-alpha.0.478

Components

Select

One option from a list, or several. The browser's own select, a listbox the kit draws for options that carry more than words, or a combobox that filters as it is typed into; the options are its children, typed by the select's value.

Drawn and behaving as Flux UI's select does, measured against its live docs.

Select

The browser's own select, in the input's box. For a list of up to five, radios or checkboxes may serve better.

SelectExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Placeholder("Choose industry...").Id("ui-select-native")[
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text.Data("testid", "ui-select-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
        ];
}
Live result

Nothing chosen.

Small

SelectSmallExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectSmallExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Sm.Placeholder("Choose industry...").Class("max-w-sm")[
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result

Option groups

Related options under a label, to make a long list easier to scan.

SelectGroupsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectGroupsExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Placeholder("Choose an industry...").Class("max-w-sm")[
            Ui.SelectGroup.Label("Creative")[
                Ui.SelectOption.Value("photography")["Photography"],
                Ui.SelectOption.Value("design")["Design services"]
            ],
            Ui.SelectGroup.Label("Technology")[
                Ui.SelectOption.Value("web-development")["Web development"],
                Ui.SelectOption.Value("it-consulting")["IT consulting"]
            ],
            Ui.SelectGroup.Label("Services")[
                Ui.SelectOption.Value("accounting")["Accounting"],
                Ui.SelectOption.Value("legal")["Legal services"],
                Ui.SelectOption.Value("consulting")["Consulting"]
            ]
        ];
}
Live result

Custom select

Listbox draws the list instead of handing it to the browser, for options with icons, pictures and more.

SelectListboxExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectListboxExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Placeholder("Choose industry...").Id("ui-select-listbox")[
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text.Data("testid", "ui-listbox-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
        ];
}
Live result
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Nothing chosen.

The button slot

Ui.SelectButton as a child sets what the select's own steps do not reach.

SelectButtonSlotExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectButtonSlotExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Class("max-w-sm")[
            Ui.SelectButton.Class("rounded-full!").Placeholder("Choose industry..."),
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Clearable

A button that puts the select back to nothing chosen, shown while there is an answer.

SelectClearableExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectClearableExample : Component
{
    private string? Industry { get; set; } = "Photography";

    protected override Component? Render() =>
        Ui.Select.Bind(() => Industry).Listbox.Clearable().Placeholder("Choose industry...").Class("max-w-sm")[
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

With a prefix

Words that stay in the button before the chosen option.

SelectPrefixExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectPrefixExample : Component
{
    private string Comparison { get; set; } = "last-month";

    protected override Component? Render() =>
        Ui.Select.Bind(() => Comparison).Listbox.Prefix("Compare to").Class("max-w-sm")[
            Ui.SelectOption.Value("previous-period")["Previous period"],
            Ui.SelectOption.Value("last-month")["Last month"],
            Ui.SelectOption.Value("last-quarter")["Last quarter"],
            Ui.SelectOption.Value("last-year")["Last year"]
        ];
}
Live result
Previous period
Last month
Last quarter
Last year

With icons

SelectIconsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectIconsExample : Component
{
    private string? _method;

    protected override Component? Render() =>
        Ui.Select.Value(_method).OnChange(value => { _method = value; }).Listbox.Placeholder("Choose method...").Class("max-w-sm")[
            Ui.SelectOption.Value("card").Label("Credit card").Icon(Ui.IconName.CreditCard),
            Ui.SelectOption.Value("paypal").Label("PayPal").Icon(Ui.IconName.Banknotes),
            Ui.SelectOption.Value("bank").Label("Bank transfer").Icon(Ui.IconName.BuildingLibrary)
        ];
}
Live result
Credit card
PayPal
Bank transfer

With descriptions

A description is the list's only: the button does not repeat it.

SelectDescriptionsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectDescriptionsExample : Component
{
    private string? _plan;

    protected override Component? Render() =>
        Ui.Select.Value(_plan).OnChange(value => { _plan = value; }).Listbox.Placeholder("Choose plan...").Class("max-w-sm")[
            Ui.SelectOption.Value("basic").Label("Basic").Description("For individuals getting started"),
            Ui.SelectOption.Value("pro").Label("Pro").Description("For small teams that need more power"),
            Ui.SelectOption.Value("enterprise").Label("Enterprise").Description("Advanced controls and support")
        ];
}
Live result
Basic

For individuals getting started

Pro

For small teams that need more power

Enterprise

Advanced controls and support

With avatars

SelectAvatarsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectAvatarsExample : Component
{
    private string? _person;

    protected override Component? Render() =>
        Ui.Select.Value(_person).OnChange(value => { _person = value; }).Listbox.Placeholder("Search people...").Class("max-w-sm")[
            Ui.SelectOption.Value("maralind").Label("Mara Lind").Avatar("/img/avatars/amber.svg"),
            Ui.SelectOption.Value("theobrandt").Label("Theo Brandt").Avatar("/img/avatars/sky.svg"),
            Ui.SelectOption.Value("rosavidal").Label("Rosa Vidal").Avatar("/img/avatars/rose.svg")
        ];
}
Live result
Mara Lind
Mara Lind
Theo Brandt
Theo Brandt
Rosa Vidal
Rosa Vidal

With custom content

An option's children are drawn as they are.

SelectCustomContentExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectCustomContentExample : Component
{
    private string? _color;

    protected override Component? Render() =>
        Ui.Select.Value(_color).OnChange(value => { _color = value; }).Listbox.Placeholder("Select color...").Class("max-w-sm")[
            Ui.SelectOption.Value("red")[
                Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-red-500"), "Red"]
            ],
            Ui.SelectOption.Value("orange")[
                Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-orange-500"), "Orange"]
            ],
            Ui.SelectOption.Value("amber")[
                Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-amber-500"), "Amber"]
            ],
            Ui.SelectOption.Value("yellow")[
                Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-yellow-500"), "Yellow"]
            ]
        ];
}
Live result
Red
Orange
Amber
Yellow

Customizing the dropdown width

The list is as wide as its button; OptionsClass widens it.

SelectWidthExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectWidthExample : Component
{
    private string? _visibility;

    protected override Component? Render() =>
        Ui.Select.Value(_visibility).OnChange(value => { _visibility = value; }).Listbox.Placeholder("Select visibility...").Class("max-w-32").OptionsClass("min-w-72")[
            Ui.SelectOption.Value("public").Label("Public").Icon(Ui.IconName.GlobeAlt)
                .Description("Shown on your calendar and eligible to be featured on our homepage."),
            Ui.SelectOption.Value("private").Label("Private").Icon(Ui.IconName.LockClosed)
                .Description("Unlisted. Only invited people and people with the link can register.")
        ];
}
Live result
Public

Shown on your calendar and eligible to be featured on our homepage.

Private

Unlisted. Only invited people and people with the link can register.

Searchable select

A search field over the options, for a long list.

SelectSearchableExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectSearchableExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Searchable().Placeholder("Choose industries...").Id("ui-select-searchable")[
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text.Data("testid", "ui-select-search-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
        ];
}
Live result
No results found
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Nothing chosen.

The search slot

Ui.SelectSearch as a child sets the search field's own steps.

SelectSearchSlotExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectSearchSlotExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Searchable().Placeholder("Choose industry...").Class("max-w-sm")[
            Ui.SelectSearch.Class("px-4").Placeholder("Search industries..."),
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result
No results found
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Search keywords

Search for "apple", "orange" or "pear": Fruit is found by words it never shows.

SelectKeywordsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectKeywordsExample : Component
{
    private string? _category;

    protected override Component? Render() =>
        Ui.Select.Value(_category).OnChange(value => { _category = value; }).Listbox.Searchable().Placeholder("Choose category...").Class("max-w-sm")[
            Ui.SelectOption.Value("fruit").Keywords("apple orange pear")["Fruit"],
            Ui.SelectOption.Value("vegetables").Keywords("carrot broccoli spinach")["Vegetables"],
            Ui.SelectOption.Value("drinks").Keywords("coffee tea juice")["Drinks"]
        ];
}
Live result
No results found
Fruit
Vegetables
Drinks

Multiple select

Opened on a collection, the listbox picks several and stays open while you do.

SelectMultipleExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectMultipleExample : Component
{
    private List<string> Industries { get; set; } = [];

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Bind(() => Industries).Live().Listbox.Multiple().Placeholder("Choose industries...").Id("ui-select-multiple")[
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text.Data("testid", "ui-multiselect-state")[
                Industries.Count == 0 ? "Nothing chosen." : $"Chosen: {string.Join(", ", Industries)}."
            ]
        ];
}
Live result
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Nothing chosen.

Selected suffix

What follows the count once several are picked: "2 industries selected".

SelectSelectedSuffixExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectSelectedSuffixExample : Component
{
    private List<string> Industries { get; set; } = ["Photography", "Accounting"];

    protected override Component? Render() =>
        Ui.Select.Bind(() => Industries).Listbox.SelectedSuffix("industries selected").Multiple().Class("max-w-sm")[
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Clearing search

Clear(Ui.SelectClear.Close) keeps the search through each pick and empties it when the list closes.

SelectClearSearchExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectClearSearchExample : Component
{
    private List<string> Industries { get; set; } = [];

    protected override Component? Render() =>
        Ui.Select.Bind(() => Industries).Listbox.Searchable().Multiple().Clear(Ui.SelectClear.Close)
            .Placeholder("Choose industries...").Class("max-w-sm")[
            Ui.SelectOption["Photography"],
            Ui.SelectOption["Design services"],
            Ui.SelectOption["Web development"],
            Ui.SelectOption["Accounting"],
            Ui.SelectOption["Legal services"],
            Ui.SelectOption["Consulting"],
            Ui.SelectOption["Other"]
        ];
}
Live result
No results found
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Combobox

A text input that filters the list under it as you type.

SelectComboboxExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectComboboxExample : Component
{
    private string? _industry;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Combobox.Placeholder("Choose industry...").Id("ui-select-combobox")[
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text.Data("testid", "ui-combobox-state")[$"Industry: {_industry ?? "none"}."]
        ];
}
Live result
No results found
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

Industry: none.

The input slot

Ui.SelectInput as a child sets the input's own steps, and can hand its text to the page.

SelectInputSlotExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectInputSlotExample : Component
{
    private string _search = "";
    private string? _industry;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Combobox[
                Ui.SelectInput.Value(_search).OnInput(text => { _search = text; }).Placeholder("Choose industry..."),
                Ui.SelectOption["Photography"],
                Ui.SelectOption["Design services"],
                Ui.SelectOption["Web development"],
                Ui.SelectOption["Accounting"],
                Ui.SelectOption["Legal services"],
                Ui.SelectOption["Consulting"],
                Ui.SelectOption["Other"]
            ],
            Ui.Text[$"The page holds the search: \"{_search}\"."]
        ];
}
Live result
No results found
Photography
Design services
Web development
Accounting
Legal services
Consulting
Other

The page holds the search: "".

Backend search

Filter(false) leaves the rows to the page, and Debounce asks it once typing has paused: one query for a word, not one for a letter.

SelectBackendSearchExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectBackendSearchExample : Component
{
    // What a store would hold. The page answers each search with the rows that match.
    private static readonly (int Id, string Name)[] Projects =
        [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure"), (4, "Documentation"), (5, "Security"), (6, "Performance")];

    private string _search = "";
    private int _searches;
    private int? _projectId;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_projectId).OnChange(value => { _projectId = value; }).Combobox.Filter(false).Id("ui-select-backend")[
                Ui.SelectInput.Placeholder("Start typing...").OnInput(Search).Debounce(250.Milliseconds),
                Projects
                    .Where(project => project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
                    .Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name])
            ],
            Ui.Text.Data("testid", "ui-select-backend-state")[$"Project: {NameOf(_projectId)}. Searches: {_searches}."]
        ];

    private void Search(string text)
    {
        _searches++;
        _search = text;
    }

    private static string NameOf(int? id) => id is { } found ? Array.Find(Projects, project => project.Id == found).Name : "none";
}
Live result
No results found
Branding
Analytics
Infrastructure
Documentation
Security
Performance

Project: none. Searches: 0.

Backend search in a listbox

The same in a searchable listbox, through Ui.SelectSearch. The rows drawn stay until the answer is here, and a row picked while the pause runs is the row you pressed.

SelectListboxSearchExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectListboxSearchExample : Component
{
    // What a store would hold: more rows than a list shows, so the page answers a search with the first few.
    private static readonly string[] Cities =
    [
        "Amsterdam", "Athens", "Berlin", "Bern", "Brussels", "Budapest", "Copenhagen", "Dublin", "Helsinki", "Lisbon",
        "Ljubljana", "Madrid", "Oslo", "Paris", "Prague", "Riga", "Rome", "Stockholm", "Vienna", "Warsaw",
    ];

    private string[] _cities = [.. Cities.Take(6)];
    private int _queries;
    private string? _city;

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Value(_city).OnChange(value => { _city = value; }).Listbox.Searchable().Filter(false).Placeholder("Choose a city...").Id("ui-select-store")[
                Ui.SelectSearch.Placeholder("Search cities...").OnInput(Search).Debounce(250.Milliseconds),
                _cities.Select(name => Ui.SelectOption.Key(name)[name])
            ],
            Ui.Text.Data("testid", "ui-select-store-state")[$"City: {_city ?? "none"}. Queries: {_queries}."]
        ];

    // The page's answer to the search: a query, here over a list.
    private void Search(string text)
    {
        _queries++;
        _cities = [.. Cities.Where(name => name.Contains(text, StringComparison.CurrentCultureIgnoreCase)).Take(6)];
    }
}
Live result
No results found
Amsterdam
Athens
Berlin
Bern
Brussels
Budapest

City: none. Queries: 0.

Create option

Type a name no project has: the create row offers to make it, from two characters on.

SelectCreateExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectCreateExample : Component
{
    private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];

    private int? ProjectId { get; set; }

    protected override Component? Render() =>
        Div.Class("max-w-sm space-y-3")[
            Ui.Select.Bind(() => ProjectId).Live().Combobox.Id("ui-select-create")[
                Ui.SelectInput.Placeholder("Start typing..."),
                _projects.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
                Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
            ],
            Ui.Text.Data("testid", "ui-select-create-state")[
                $"Project: {(ProjectId is { } id ? _projects.Find(project => project.Id == id).Name : "none")}."
            ]
        ];

    // The page's answer to the create row: add the project, and pick it.
    private void CreateProject(string name)
    {
        var id = _projects.Max(project => project.Id) + 1;
        _projects.Add((id, name));
        ProjectId = id;
    }
}
Live result
No results found
Branding
Analytics
Infrastructure
Create new project

Project: none.

With backend search

When the page filters, keep the chosen project among the rows, so the select can go on naming it.

SelectCreateSearchExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectCreateSearchExample : Component
{
    private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
    private string _search = "";

    private int? ProjectId { get; set; }

    protected override Component? Render() =>
        Ui.Select.Bind(() => ProjectId).Combobox.Filter(false).Class("max-w-sm")[
            Ui.SelectInput.Placeholder("Start typing...").OnInput(text => { _search = text.Trim(); }),
            Matching().Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
            Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
        ];

    // The rows that match the search, and the one picked even when it does not: the select goes on naming it.
    private IEnumerable<(int Id, string Name)> Matching() =>
        _projects.Where(project => project.Id == ProjectId || project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase));

    private void CreateProject(string name)
    {
        var id = _projects.Max(project => project.Id) + 1;
        _projects.Add((id, name.Trim()));
        ProjectId = id;
    }
}
Live result
No results found
Branding
Analytics
Infrastructure
Create new project

Loading message

What the list says while a slow search is on its way, and when it finds nothing.

SelectLoadingExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectLoadingExample : Component
{
    private static readonly (int Id, string Name)[] Projects =
        [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure"), (4, "Documentation")];

    private string _search = "";
    private int? _projectId;

    protected override Component? Render() =>
        Ui.Select.Value(_projectId).OnChange(value => { _projectId = value; }).Combobox.Filter(false).Class("max-w-sm")[
            Ui.SelectInput.Placeholder("Start typing...").OnInput(Search).Debounce(250.Milliseconds),
            Projects
                .Where(project => project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
                .Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
            Ui.SelectOptionEmpty.WhenLoading("Loading projects...")["No projects found."]
        ];

    // A slow store: long enough to read the loading message.
    private async Task Search(string text)
    {
        await Task.Delay(600.Milliseconds);
        _search = text;
    }
}
Live result
No projects found.
Branding
Analytics
Infrastructure
Documentation

With validation

The page refuses a name longer than forty characters: the input is marked invalid until the next search.

SelectValidationExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectValidationExample : Component
{
    private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
    private string _search = "";
    private string? _error;

    private int? ProjectId { get; set; }

    protected override Component? Render() =>
        Ui.Field.Class("max-w-sm")[
            Ui.Label["Project"],
            Ui.Select.Bind(() => ProjectId).Combobox.Filter(false).ShowValidation(false)[
                Ui.SelectInput.Placeholder("Start typing...").Invalid(_error is not null).OnInput(Search),
                _projects
                    .Where(project => project.Id == ProjectId || project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
                    .Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
                Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
            ],
            Ui.Error.Message(_error)
        ];

    // A new search clears the last refusal, as Flux's example resets its error bag.
    private void Search(string text)
    {
        _search = text.Trim();
        _error = null;
    }

    private void CreateProject(string name)
    {
        name = name.Trim();
        if (name.Length > 40)
        {
            _error = "A project's name is 40 characters at most.";
            return;
        }

        var id = _projects.Max(project => project.Id) + 1;
        _projects.Add((id, name));
        ProjectId = id;
    }
}
Live result
No results found
Branding
Analytics
Infrastructure
Create new project

With modal

A create row in a listbox opens a form of the page's own in a modal.

SelectModalExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class SelectModalExample : Component
{
    private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
    private bool _creating;

    private int? ProjectId { get; set; }

    private string ProjectName { get; set; } = "";

    protected override Component? Render() =>
        Div.Class("max-w-sm")[
            Ui.Select.Bind(() => ProjectId).Listbox.Placeholder("Choose project...")[
                _projects.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
                Ui.SelectOptionCreate.OnClick(_ => { _creating = true; })["Create new"]
            ],
            Ui.Modal.Open(_creating).OnClose(() => { _creating = false; }).Class("md:w-96")[
                Div.Class("space-y-6")[
                    Div[
                        Ui.Heading.Level(2).Lg["Create new project"],
                        Ui.Text.Class("mt-2")["Enter the name of the new project."]
                    ],
                    Ui.Input.Bind(() => ProjectName).Label("Name").Placeholder("e.g. 'UX Research'"),
                    Div.Class("flex")[
                        Ui.Spacer,
                        Ui.Button.Primary.OnClick(CreateProject)["Create"]
                    ]
                ]
            ]
        ];

    private void CreateProject()
    {
        if (string.IsNullOrWhiteSpace(ProjectName))
        {
            return;
        }

        var id = _projects.Max(project => project.Id) + 1;
        _projects.Add((id, ProjectName.Trim()));
        ProjectId = id;
        ProjectName = "";
        _creating = false;
    }
}
Live result
Branding
Analytics
Infrastructure
Create new

Create new project

Enter the name of the new project.

Behaviour

Options are children

Bind or Value stands where Flux has wire:model, and the options are the select's children, typed by its value: Ui.Select.Bind(() => m.Country)[countries.Select(c => Ui.SelectOption.Key(c.Code).Value(c.Code)[c.Name])]. An option with no Value stands for its own text. Options made from a list take a Key.

The native variant is the browser's <select>: it needs no runtime, renders complete on a prerendered page and gets a phone's own picker, and its options hold text and nothing else. A listbox's button keeps the chosen option's words, icon and avatar for as long as the value is that one, whatever rows are drawn, and SelectedLabel is what it shows when that should not be the option's own words. An Avatar wins over an Icon. Position and Align say which side the list opens on and which edge it lines up with.

Searching in the browser

Searchable() puts a search field at the top of the listbox's options. Opening focuses it; typing filters case- and accent-insensitively over each option's text and its Keywords, and puts the cursor on the first match. "No results found" shows when nothing matches, and Empty("…") says it in your own words. The filtering is the browser's, as Flux's is: a key typed sends the page nothing.

In a combobox a click or typing opens the list, Enter picks and the input shows the answer. Escape closes the list and a second Escape empties the text but keeps the answer; leaving the input with words that name nothing puts the answer's text back.

Searching on the server

Filter(false) hands the search to the page: the OnInput of Ui.SelectSearch or Ui.SelectInput hears what is typed, at every key or at the pause Debounce(250.Milliseconds) names, and the page renders the rows that match. The browser counts the pause, so nothing is sent while the reader types. The rows drawn stay until the new ones are here, and a row pressed (or Enter) during the pause picks from the list as it is drawn; the text typed since is dropped with it.

The page's search follows the field. When the select rewrites the field itself (the list shuts and the field is emptied, or a combobox's input says the answer), OnInput hears that text too, once, and only when the page thinks otherwise: after a search for "ber" and a pick it is called with "", so the page goes back to the rows it starts from.

Ui.SelectOptionCreate offers to make an option of what was typed once the search is MinLength long and names none that exists; its OnClick is handed the text, and the page adds the option and chooses it. In a listbox with no search it is always shown and handed an empty string: the case where the page opens a form of its own. Ui.SelectOptionEmpty says what the list says when there are no rows, and WhenLoading what it says while results are on their way.

Several answers

Open the select on a collection (Bind over a List<T>, a T[] or a HashSet<T>, or Values(tags) with OnChange) and .Listbox.Multiple() picks several. A pick switches its row and leaves the list open; the button names the one option picked, or counts them ("3 selected", or your SelectedSuffix). A bound list, array or set each gets its own kind back. Several answers are the listbox's, as on Flux: Multiple() over one value, or a collection on the native variant, throws and says so.

The keyboard

The keys are the runtime's (data-rask-list), recorded on fluxui.dev and matched, and an open list sends the page nothing until a row is picked.

Key
What happens
Click, Space, ArrowDown, ArrowUpOpen, with the cursor on the chosen option, or the first when there is none.
ArrowDown, ArrowUpMove the cursor past a disabled option. They stop at either end; there is no wrap.
Home, End, PageUp, PageDownNothing, as on Flux.
EnterPicks the lit row and closes; with Multiple() it switches the row and the list stays open. On the closed button it does nothing.
Escape, a click elsewhereClose; the focus returns to the button.
TabCloses, picks nothing, and moves on.
A letterOn the closed button it picks the next option starting with it; on the open list it moves the cursor. Letters within half a second are one prefix.
The pointerLights the row it enters; a row it lit goes dark when it leaves the list, and one the keys lit stays.

No script of the kit's

The drawn list is a native popover placed by CSS anchor positioning: as wide as its trigger, 5px under it, at most 20rem tall, and kept inside the viewport. The browser opens it on a click and closes it on Escape or a click elsewhere. While it is open the page behind it neither scrolls nor takes the pointer (data-rask-lock), and the closed button ignores Enter and its arrows do not scroll the page (data-rask-listbox-button), as Flux's script does.

The trigger carries role="combobox", aria-haspopup="listbox", aria-expanded, aria-controls and, while open, aria-activedescendant; the list is role="listbox" and each row role="option" with aria-selected. A drawn select posts nothing by itself, so one with a Name carries its answer in hidden inputs. A press on a row names its own row, and a bound select then writes its member with the next action, or at once with .Live().

Reference

Ui.Select

flux:select

One option from a list, or several when it is opened on a collection. Its options are its children.

Step
Takes
What it does
.Bind(…)() => a TThe model's member it reads and writes: () => m.Country, or a list for several. Or Value with OnChange.
.Value(…)T, the options' typeThe chosen option's value, for a page that holds it itself. Values(list) for several.
.OnChange(…)a handler given the new valueRuns with the new value when an option is picked.
.AfterBind(…)a handler given the new valueRuns after Bind has written the model.
.Validate(…)a validatorA rule the value is checked by when it is written.
.Live()boolWrites the model as soon as an option is picked, not with the next action.
.Variant(…)
Ui.SelectVariant
Default
Listbox
Combobox

Each is a step of its own too, unless an HTML tag has its name.

The browser's own select, a drawn listbox, or a combobox. The browser's when unset.
.Size(…)
Ui.SelectSize
Base
Sm
Xs

Each is a step of its own too, unless an HTML tag has its name.

How tall it is. 40px when unset.
.Placeholder(…)stringShown while nothing is picked.
.Label(…)stringDraws a field around it, with this label and the error.
.Description(…)stringHelp text between the label and the select.
.DescriptionTrailing(…)stringHelp text under the select.
.Badge(…)stringA word beside the label: "Optional".
.Multiple()boolPicks several, over a collection. The listbox's.
.Searchable()boolA search field over the listbox's options.
.Filter()boolFalse leaves every option in the list whatever is typed: the page filters, from the search's OnInput.
.Empty(…)stringWhat a search that matches nothing says. "No results found" when unset.
.Clearable()boolA button that clears the answer, shown while there is one.
.Prefix(…)stringWords that stay in the button before the chosen option: "Compare to".
.SelectedSuffix(…)stringWhat follows the count once several are picked. "selected" when unset.
.Clear(…)
Ui.SelectClear
Select
Close

Each is a step of its own too, unless an HTML tag has its name.

When a search empties itself: at every pick, or when the list closes.
.Position(…)
Ui.Position
Default
Top
Right
Bottom
Left

Each is a step of its own too, unless an HTML tag has its name.

Which side of the select the list opens on. Below when unset.
.Align(…)
Ui.Align
Default
Start
Center
End

Each is a step of its own too, unless an HTML tag has its name.

Which edge of the select the list lines up with. Its start when unset.
.OptionsClass(…)stringClasses for the open list: a width, such as min-w-72.
.Disabled()boolA select that cannot be opened or changed.
.Invalid()boolError styling the form did not ask for.
.Name(…)stringWhat the answer is posted under in a form.
.ShowValidation()boolFalse leaves the member's message to a Ui.Error placed elsewhere.

Ui.SelectOption

flux:select.option

One answer. Its children are its words, or content of your own.

Step
Takes
What it does
.Value(…)a value of the select's typeWhat the select holds while it is chosen. Its own words when unset.
.Label(…)stringIts words, in place of children.
.SelectedLabel(…)stringWhat the button says while it is chosen, when that is not its own words.
.Keywords(…)stringMore words a search finds it by, never shown: "apple orange pear".
.Icon(…)Ui.IconName — one of 317 namesAn icon at its start, in a listbox or a combobox.
.IconVariant(…)
Ui.IconVariant
Outline
Solid
Mini
Micro

Each is a step of its own too, unless an HTML tag has its name.

Which drawing of the icon. Mini when unset.
.IconClass(…)stringClasses for the icon.
.Avatar(…)stringA picture's address, drawn as an avatar at its start. It wins over an icon.
.Description(…)stringWords under its label in the list, not in the button.
.Disabled()boolShown but not pickable.

Ui.SelectGroup

flux:select.group

Options under a heading: an <optgroup> in the browser's select.

Step
Takes
What it does
.Label(…)stringThe heading over its options.

Ui.SelectButton

flux:select.button

The listbox's button, as a child, for what the select's steps do not reach.

Step
Takes
What it does
.Placeholder(…)stringShown while nothing is picked.
.Invalid()boolError styling.
.Size(…)
Ui.SelectSize
Base
Sm
Xs

Each is a step of its own too, unless an HTML tag has its name.

How tall it is.
.Disabled()boolA button that cannot be pressed.
.Clearable()boolA button that clears the answer.

Ui.SelectInput

flux:select.input

The combobox's input, as a child, and what is typed into it.

Step
Takes
What it does
.Placeholder(…)stringShown while it is empty.
.Invalid()boolError styling: a refused search.
.Size(…)
Ui.SelectSize
Base
Sm
Xs

Each is a step of its own too, unless an HTML tag has its name.

How tall it is.
.Value(…)stringThe text, for a page that holds the search itself.
.OnInput(…)a handler given the new valueRuns with the text as typed, or once typing has paused for Debounce.
.Debounce(…)a TimeSpan: 250.MillisecondsHow long typing pauses before OnInput runs.

Ui.SelectSearch

flux:select.search

The listbox's search field, as a child.

Step
Takes
What it does
.Placeholder(…)stringShown while it is empty. "Search..." when unset.
.Icon(…)Ui.IconName — one of 317 namesThe icon at its start. A magnifying glass when unset.
.Clearable()boolThe button that empties it. Shown unless false.
.Value(…)stringThe text, for a page that holds the search itself.
.OnInput(…)a handler given the new valueRuns with the text as typed, or once typing has paused for Debounce.
.Debounce(…)a TimeSpan: 250.MillisecondsHow long typing pauses before OnInput runs.

Ui.SelectOptionCreate

flux:select.option.create

The row that offers to make an option of what was typed. Its children are its words.

Step
Takes
What it does
.MinLength(…)intHow long the search must be before it is offered.
.OnClick(…)a handler given the textRuns with the search as typed when it is picked.

Ui.SelectOptionEmpty

flux:select.option.empty

What the list says when a search finds nothing. Its children are its words.

Step
Takes
What it does
.WhenLoading(…)stringWhat it says while results are on their way.
Confirm