Files
Htmx/docs/Components/Select.md
T
2026-05-04 19:57:48 +05:00

6.5 KiB
Raw Blame History

Select

A styled <select> dropdown. Supports a pre-selected value, optional label, and optional description text. HTMX attributes can be added.


HTML structure

div.flex.flex-col.gap-1.5
  label[for={id}].text-sm.font-medium        ← omitted when label is empty
    {label}
  select[id, name, class, $$HxAttrs$$]
    option[value, $$Selected$$]              ← one per option; selected="selected" when matched
      {display}
  p.text-sm.text-muted-foreground            ← omitted when description is empty
    {description}

CSS mechanics

Class Effect
flex h-10 w-full rounded-md border border-input bg-background Full-width 40px select field
px-3 py-2 text-sm Inner padding and text size
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring Keyboard focus ring
disabled:cursor-not-allowed disabled:opacity-50 Disabled state
bg-background Ensures the dropdown matches the page background in dark mode

Constructor signature

public Select(
    string id,
    IEnumerable<(string Value, string Display)> options,
    string? selectedValue = null,
    string name           = "",
    string label          = "",
    string description    = "",
    string extraClasses   = "",
    string hxAttrs        = "")
Parameter Description
id Element id and label for target
options List of (Value, Display) tuples
selectedValue Pre-selected option value; null = no pre-selection (first option shown)
name Form field name
label Optional visible label
description Optional helper text below the field
extraClasses Additional Tailwind classes on the <select> element
hxAttrs Verbatim HTMX / data attributes

Usage examples

Country selector

new Select(
    id:   "country",
    name: "country",
    label: "Country",
    options: new[]
    {
        ("us", "United States"),
        ("gb", "United Kingdom"),
        ("ca", "Canada"),
        ("au", "Australia"),
    },
    selectedValue: "us")

Dynamic options from data

var options = categories.Select(c => (c.Slug, c.Name));

new Select(
    id:    "category",
    name:  "category",
    label: "Category",
    options: options,
    selectedValue: existingCategory)

HTMX on-change reload

new Select(
    id:      "region",
    name:    "region",
    label:   "Region",
    options: regions,
    hxAttrs: """hx-get="/cities" hx-target="#city-select" hx-trigger="change" hx-include="[name='region']"""")

Reading in a form handler

public record Command([property: FromForm] string Country);

// command.Country == "us" | "gb" | "ca" | "au"

Placeholder option (no pre-selection)

new Select(
    id:   "role",
    name: "role",
    label: "Role",
    options: new[]
    {
        ("",      "— Select a role —"),
        ("admin", "Administrator"),
        ("user",  "Regular user"),
    },
    selectedValue: "")

Tips and tricks

  • Pass an empty-value placeholder as the first option (("", "Select…")) to force the user to make an explicit selection.
  • selectedValue comparison is exact — make sure the value you pass matches one of the Value strings in options.
  • hxAttrs is verbatim — you can add multiple, size, disabled, autocomplete, or any other native attribute here.
  • To conditionally disable individual options, build the raw <select> HTML manually or subclass the component.
  • To conditionally disable individual options, build the raw <select> HTML manually or subclass the component.

Complete page example

Templates/FilterProductsPage.htmx

<div class="max-w-4xl mx-auto py-10">
  <h1 class="text-2xl font-bold mb-6">Products</h1>
  <form class="flex gap-4 mb-8 items-end"
        hx-get="/products/filter"
        hx-target="#product-list"
        hx-trigger="change">
    $$CategorySelect$$
    $$SortSelect$$
  </form>
  <div id="product-list">
    $$ProductTable$$
  </div>
</div>

Templates/FilterProductsPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class FilterProductsPage : FilterProductsPageBase
{
    private readonly IHtmxComponent _category;
    private readonly IHtmxComponent _sort;
    private readonly IHtmxComponent _table;

    public FilterProductsPage(
        IEnumerable<Product> products,
        string selectedCategory = "",
        string selectedSort     = "name-asc")
    {
        _category = new Components.Select(
            id:   "category",
            name: "category",
            label: "Category",
            options: new[]
            {
                ("",           "All categories"),
                ("electronics","Electronics"),
                ("clothing",   "Clothing"),
                ("books",      "Books"),
            },
            selectedValue: selectedCategory);

        _sort = new Components.Select(
            id:   "sort",
            name: "sort",
            label: "Sort by",
            options: new[]
            {
                ("name-asc",   "Name AZ"),
                ("name-desc",  "Name ZA"),
                ("price-asc",  "Price: low to high"),
                ("price-desc", "Price: high to low"),
            },
            selectedValue: selectedSort);

        _table = new Components.Table(
            headers: new[] { "Name", "Category", "Price" },
            rows: products.Select(p => new[]
            {
                System.Net.WebUtility.HtmlEncode(p.Name),
                System.Net.WebUtility.HtmlEncode(p.Category),
                $"${p.Price:F2}",
            }));
    }

    protected override void RenderCategorySelect(HtmxRenderContext ctx) => _category.Render(ctx.Next());
    protected override void RenderSortSelect(HtmxRenderContext ctx)     => _sort.Render(ctx.Next());
    protected override void RenderProductTable(HtmxRenderContext ctx)   => _table.Render(ctx.Next());
}

GET handler (full page + HTMX partial)

[Handler]
[MapGet("/products")]
public static partial class GetProductsHandler
{
    public record Query(
        [property: FromQuery] string Category = "",
        [property: FromQuery] string Sort     = "name-asc");

    private static async Task<IResult> HandleAsync(
        Query q, HttpContext ctx, ProductService products, CancellationToken ct)
    {
        var items = await products.FilterAsync(q.Category, q.Sort, ct);
        return await ctx.WriteHtmxPage(
            new FilterProductsPage(items, q.Category, q.Sort), title: "Products");
    }
}