Dropdowns
Alpine custom dropdown — trigger button (label + chevron) + panel that opens on click and closes on click.outside. Options go inside as child content using koala-dropdown-option.
Canonical
<koala-dropdown placeholder="Select a partner">
<button type="button" koala-dropdown-option x-on:click="open = false">Acme Legal</button>
<button type="button" koala-dropdown-option x-on:click="open = false">Bramble & Co</button>
<button type="button" koala-dropdown-option x-on:click="open = false">Carter Conveyancing</button>
</koala-dropdown>
Use for any small, finite list of options. Options pair with x-on:click="open = false"
to dismiss the panel after selection.
Variants
3 variants<koala-dropdown placeholder="Pick a branch">
<form method="post" asp-page-handler="SelectBranch">
<input type="hidden" name="BranchId" value="@branch.Id" />
<button type="submit" koala-dropdown-option>@branch.Name</button>
</form>
</koala-dropdown>
States
3 statesProps
4 attributes| Attribute | Values | Notes |
|---|---|---|
| placeholder | string | Defaults to Select…. Shown in muted colour until a selection is made. |
| selected-label | string? | When present, replaces the placeholder with the current selection in default text colour. |
| trigger-icon | IconName | Icon inside the trigger. Defaults to ChevronDown; override to Funnel for filter dropdowns. |
| trigger-id | string? | Optional id on the trigger <button>. Set when other elements need aria-controls or label-for relationships. |
| class | string? | Extra classes appended to the wrapping <div>. |
| koala-dropdown-option | attribute on option | Applies hover + rounded styling. Pair with x-on:click="open = false" for non-form options to dismiss the panel. |
Do & don't
Multi-select variant
<koala-multi-dropdown> uses the same chrome (trigger,
click-outside close, Escape, aria-expanded) but the panel renders checkbox-style options that don't
close it on click. Use for filter rows where the user picks multiple values.
<koala-multi-dropdown label="Status" count="@selectedCount" trigger-icon="CircleDot">
<label class="flex items-center px-3 py-2 hover:bg-surface-dim cursor-pointer">
<input type="checkbox" name="Status" value="New" class="w-4 h-4 mr-2 text-primary border-outline focus:ring-primary" />
<span class="text-on-surface">New</span>
</label>
</koala-multi-dropdown>
| Attribute | Values | Notes |
|---|---|---|
| label | String | Trigger label (e.g. "Status"). Default Filter. |
| count | int? | Number of values selected — appears in the trigger as "Status (3)" when > 0. |
| trigger-icon | IconName | Default Funnel. Pick something more specific where it helps (CircleDot for status filters). |
| trigger-id | String | Optional. Useful for x-target swaps that re-render the trigger with the new count. |
Form-bound (enum)
Set for to bind an enum model property. The dropdown then renders a
hidden input (so server-side model binding + validation pick up the value), auto-generates its options
from the enum values' [Display(Name = "…")] names, and shows a
validation message. The Unknown sentinel is filtered out by default
(override via exclude-values). This replaces the former
<koala-enum-field> helper.
<koala-dropdown for="Input.Crm" label="CRM" placeholder="Select CRM" optional />
Bound to Input.Crm (a DemoCrm? enum). Click to see each value's
[Display(Name)] label.
| Attribute | Values | Notes |
|---|---|---|
| for | Model expression | Switches on form-bound mode. Must resolve to an enum type (nullable supported). |
| label | String | Optional. Renders a <label> above the dropdown. |
| optional | Boolean | Appends (optional) to the label. |
| exclude-values | Comma-separated identifiers | Default Unknown. Override to filter additional values out of the option list. |
| class | Tailwind classes | In form-bound mode, defaults to mb-5. Override for tight layouts. |
Sort (form-submit)
Set submit-options (an
IReadOnlyList<ListSortOption>) +
submit-name to turn the panel into a sort control. This is a sort
control whose options submit the list filter form so the active search + filters travel with the sort —
each option renders as a <button type="submit"> inside the
enclosing <form method="get">, posting the
submit-name query param with the option's token. Set the outer-wrapper
id and list it in the form's
x-target.push so the active label re-renders after the AJAX submit.
Pair with variant="Chip" and
trigger-icon="ArrowUpDown". Absorbs the former
<koala-dropdown-sort>.
@{
var sort = new SortDropdownModel(new[]
{
new ListSortOption("newest", "Newest first", IsActive: Model.Sort == "newest"),
new ListSortOption("oldest", "Oldest first", IsActive: Model.Sort == "oldest"),
});
}
<form method="get" x-target.push="client-results sort-dropdown">
<koala-dropdown variant="Chip" trigger-icon="ArrowUpDown"
id="@sort.Id" submit-name="@sort.FieldName" submit-options="@sort.Options"
align="right"
selected-label="@(sort.Options.FirstOrDefault(o => o.IsActive)?.Label ?? "Sort")" />
</form>
Show the active sort in the trigger label. Cursor pagination resets to page 1 because
?after= is intentionally not a form field.
| Attribute | Values | Notes |
|---|---|---|
| submit-options | IReadOnlyList<ListSortOption> | Switches on form-submit (sort) mode. The token / label / is-active set rendered as submit buttons. |
| submit-name | String | The query-string field each option submits (e.g. Sort, QuoteSort). |
| id | String | Set on the outer wrapper. List it in the form's x-target.push so the active label repaints after submit. |
| align | left | right | Panel alignment. right for sort controls anchored at the right of the list header. |
Chip trigger (variant)
Set variant="Chip" for a compact, inline outlined trigger that sits
next to a heading or inline value — rather than the full-width, input-like default
(variant="Input"). The panel still opens on click and closes on
click-outside / Escape. Pair with align="right" when the chip sits at
the right edge of its row so the panel doesn't overflow the viewport.
<koala-dropdown variant="Chip" selected-label="London office">
<button type="button" koala-dropdown-option x-on:click="open = false">London office</button>
<button type="button" koala-dropdown-option x-on:click="open = false">Bristol office</button>
</koala-dropdown>
The same child-content / items / submit-options
options work in either variant.
Model-driven items (version picker)
Pass items (an
IReadOnlyList<KoalaDropdownItem>) to build the menu from data
instead of child markup. Each item becomes a link (carrying
x-target.push for Alpine-AJAX nav); the one marked
IsActive renders as a highlighted, non-link current item. This is what
the quote and terms version pickers use — a Chip trigger showing the current version with the
full version history in the panel.
@{
var versionItems = Model.AllVersions
.OrderByDescending(v => v.Version)
.Select(v => new KoalaDropdownItem(
$"Version {v.Version}",
Href: ViewQuote.Route(v.Id),
IsActive: v.Id == Model.Quote.Id))
.ToList();
}
<koala-dropdown variant="Chip"
selected-label="@($"Version {Model.Quote.Version}")"
items="versionItems" />
Options stack as block links — the active version is highlighted at the top.
| Attribute | Values | Notes |
|---|---|---|
| items | IReadOnlyList<KoalaDropdownItem> | Switches on model-driven options. Each KoalaDropdownItem is (Label, Href?, IsActive, XTarget). |
| variant | Input | Chip | Trigger style. Input (default) is the full-width input-like button; Chip is the compact inline trigger. |
| align | left | right | Chip-variant panel alignment. right when the trigger sits at the edge of its row. |