Select
UI::SelectA styled dropdown with keyboard navigation and type-ahead, posting like a native select.
# VelvetKit
## Select (`UI::Select`)
A styled dropdown with keyboard navigation and type-ahead, posting like a native select.
Add it: `bin/rails generate velvetkit:add select`
Also adds: `label`, `form_field`, `icon`
Stimulus: `ui--select`
Installs: `@floating-ui/dom`
### Arguments
```ruby
UI::Select.new(name: nil, options: [], selected: nil, prompt: nil, label: nil, hint: nil, size: :default, searchable: false, multiple: false, required: false, error: nil, disabled: false, form: nil, field: nil, class: nil, **html_attrs)
```
### Notes
UI::Select — a dropdown for choosing one value, or several, styled to match the kit.
The visible part is a WAI-ARIA "select-only combobox": a button with
role=combobox and a floating role=listbox. Focus stays on the button while
the arrow keys move through the options, as the pattern specifies.
The value lives in a visually hidden native <select>. That keeps everything a
form already relies on: the field submits under its name, `required` blocks
submission, form reset works, the form builder binds it, and anything
listening for `change` on it still hears one. The Stimulus controller only
mirrors the choice into it.
`searchable: true` adds a search box that filters the list; `multiple: true`
lets people pick several, posting `name[]` like a native multiple select.
UI::NativeSelect is the browser's own control, for when a phone's system
picker is the better fit.
Usage:
<%= render(UI::Select.new(form: f, field: :plan, label: "Plan",
options: [["Free", "free"], ["Pro", "pro"]],
prompt: "Choose a plan")) %>
### Example: Default
```erb
<div class="max-w-xs">
<%= render(UI::Select.new(name: "plan", label: "Plan", prompt: "Choose a plan",
options: [ [ "Free", "free" ], [ "Team", "team" ], [ "Business", "business" ] ])) %>
</div>
```
### Example: States
```erb
<div class="flex max-w-xs flex-col gap-5">
<%= render(UI::Select.new(name: "a", label: "Selected", options: %w[Viewer Editor Admin], selected: "Editor")) %>
<%= render(UI::Select.new(name: "b", label: "Required", options: %w[Viewer Editor Admin], prompt: "Choose a role", required: true)) %>
<%= render(UI::Select.new(name: "c", label: "With an error", options: %w[Viewer Editor Admin], prompt: "Choose a role", error: true)) %>
<%= render(UI::Select.new(name: "d", label: "Disabled", options: %w[Viewer Editor Admin], selected: "Viewer", disabled: true)) %>
</div>
```
### Example: Sizes
```erb
<div class="flex max-w-xs flex-col gap-5">
<% %i[sm default lg].each do |size| %>
<%= render(UI::Select.new(name: "size_#{size}", label: size.to_s, size: size, options: %w[Daily Weekly Monthly], selected: "Weekly")) %>
<% end %>
</div>
```
### Example: Searchable
```erb
<%# Searchable: open it and type to filter. Arrow keys and Enter work from the search box. %>
<div class="max-w-xs">
<%= render(UI::Select.new(name: "time_zone", label: "Time zone", prompt: "Choose a time zone", searchable: true,
options: %w[Auckland Bangkok Berlin Cairo Chicago Dubai Honolulu Istanbul Jakarta Kolkata Lagos Lima London Madrid Mexico\ City Moscow Nairobi New\ York Paris Santiago Seoul Singapore Sydney Tokyo Toronto])) %>
</div>
```
### Example: Multiple
```erb
<%# Stays open while you tick; past two choices the trigger counts them. %>
<div class="max-w-xs">
<%= render(UI::Select.new(name: "reviewers", label: "Reviewers", prompt: "Add reviewers", multiple: true, searchable: true,
options: [ [ "Asha Kulkarni", 1 ], [ "Ravi Menon", 2 ], [ "Sara Joseph", 3 ], [ "Tomás Ortega", 4 ], [ "Mei Lin", 5 ] ],
selected: [ 1, 2 ])) %>
</div>
```
bin/rails generate velvetkit:add select
- Also adds
- Label Form field Icon
- Stimulus
- ui--select
- Installs
- @floating-ui/dom
Examples
Default
Choose a plan
Free
Team
Business
default.html.erb
<div class="max-w-xs">
<%= render(UI::Select.new(name: "plan", label: "Plan", prompt: "Choose a plan",
options: [ [ "Free", "free" ], [ "Team", "team" ], [ "Business", "business" ] ])) %>
</div>
States
Viewer
Editor
Admin
Choose a role
Viewer
Editor
Admin
Choose a role
Viewer
Editor
Admin
Viewer
Editor
Admin
states.html.erb
<div class="flex max-w-xs flex-col gap-5">
<%= render(UI::Select.new(name: "a", label: "Selected", options: %w[Viewer Editor Admin], selected: "Editor")) %>
<%= render(UI::Select.new(name: "b", label: "Required", options: %w[Viewer Editor Admin], prompt: "Choose a role", required: true)) %>
<%= render(UI::Select.new(name: "c", label: "With an error", options: %w[Viewer Editor Admin], prompt: "Choose a role", error: true)) %>
<%= render(UI::Select.new(name: "d", label: "Disabled", options: %w[Viewer Editor Admin], selected: "Viewer", disabled: true)) %>
</div>
Sizes
Daily
Weekly
Monthly
Daily
Weekly
Monthly
Daily
Weekly
Monthly
sizes.html.erb
<div class="flex max-w-xs flex-col gap-5">
<% %i[sm default lg].each do |size| %>
<%= render(UI::Select.new(name: "size_#{size}", label: size.to_s, size: size, options: %w[Daily Weekly Monthly], selected: "Weekly")) %>
<% end %>
</div>
Searchable
Choose a time zone
Auckland
Bangkok
Berlin
Cairo
Chicago
Dubai
Honolulu
Istanbul
Jakarta
Kolkata
Lagos
Lima
London
Madrid
Mexico City
Moscow
Nairobi
New York
Paris
Santiago
Seoul
Singapore
Sydney
Tokyo
Toronto
No matches
long_list.html.erb
<%# Searchable: open it and type to filter. Arrow keys and Enter work from the search box. %>
<div class="max-w-xs">
<%= render(UI::Select.new(name: "time_zone", label: "Time zone", prompt: "Choose a time zone", searchable: true,
options: %w[Auckland Bangkok Berlin Cairo Chicago Dubai Honolulu Istanbul Jakarta Kolkata Lagos Lima London Madrid Mexico\ City Moscow Nairobi New\ York Paris Santiago Seoul Singapore Sydney Tokyo Toronto])) %>
</div>
Multiple
Asha Kulkarni
Ravi Menon
Sara Joseph
Tomás Ortega
Mei Lin
No matches
multiple.html.erb
<%# Stays open while you tick; past two choices the trigger counts them. %>
<div class="max-w-xs">
<%= render(UI::Select.new(name: "reviewers", label: "Reviewers", prompt: "Add reviewers", multiple: true, searchable: true,
options: [ [ "Asha Kulkarni", 1 ], [ "Ravi Menon", 2 ], [ "Sara Joseph", 3 ], [ "Tomás Ortega", 4 ], [ "Mei Lin", 5 ] ],
selected: [ 1, 2 ])) %>
</div>
API
Arguments
UI::Select.new(name: nil, options: [], selected: nil, prompt: nil, label: nil, hint: nil, size: :default, searchable: false, multiple: false, required: false, error: nil, disabled: false, form: nil, field: nil, class: nil, **html_attrs)
Notes
UI::Select — a dropdown for choosing one value, or several, styled to match the kit.
The visible part is a WAI-ARIA "select-only combobox": a button with
role=combobox and a floating role=listbox. Focus stays on the button while
the arrow keys move through the options, as the pattern specifies.
The value lives in a visually hidden native <select>. That keeps everything a
form already relies on: the field submits under its name, `required` blocks
submission, form reset works, the form builder binds it, and anything
listening for `change` on it still hears one. The Stimulus controller only
mirrors the choice into it.
`searchable: true` adds a search box that filters the list; `multiple: true`
lets people pick several, posting `name[]` like a native multiple select.
UI::NativeSelect is the browser's own control, for when a phone's system
picker is the better fit.
Usage:
<%= render(UI::Select.new(form: f, field: :plan, label: "Plan",
options: [["Free", "free"], ["Pro", "pro"]],
prompt: "Choose a plan")) %>