Native Select
A styled native HTML select element with consistent design system integration.
Usage
Default
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
NativeSelectOption(value: "pineapple") { "Pineapple" }
end
end
Groups
Use NativeSelectGroup to organize options into categories.
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a department" }
NativeSelectGroup(label: "Engineering") do
NativeSelectOption(value: "frontend") { "Frontend" }
NativeSelectOption(value: "backend") { "Backend" }
NativeSelectOption(value: "devops") { "DevOps" }
end
NativeSelectGroup(label: "Sales") do
NativeSelectOption(value: "account_executive") { "Account Executive" }
NativeSelectOption(value: "sales_development") { "Sales Development" }
end
end
end
Disabled
Add the disabled attribute to the NativeSelect component to disable the select.
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(disabled: true) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
Size
Use size: :sm for a compact select. Classes passed to NativeSelect style the wrapper, so class: "w-full" stretches the whole field.
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(size: :sm, class: "w-full") do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
Invalid
Use aria-invalid to show validation errors.
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(aria: {invalid: "true"}) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
Native Select vs Select
NativeSelect: Choose for native browser behavior, superior performance, or mobile-optimized dropdowns.
Select: Choose for custom styling, animations, or complex interactions.
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component NativeSelect
Manual installation
1
Add RubyUI::NativeSelect to app/components/ruby_ui/native_select/native_select.rb
# frozen_string_literal: true
module RubyUI
class NativeSelect < Base
WRAPPER_CLASS = "group/native-select relative w-fit has-[select:disabled]:opacity-50"
# As in shadcn, class styles the wrapper <div>; every other attribute
# goes to the <select>.
def initialize(size: :default, **attrs)
@size = size
@wrapper_classes = [attrs.delete(:class), attrs.delete("class")].compact
super(**attrs)
end
def view_template(&block)
div(
class: TAILWIND_MERGER.merge(mix({class: WRAPPER_CLASS}, *@wrapper_classes.map { |c| {class: c} })[:class]),
data: {slot: "native-select-wrapper", size: @size}
) do
select(**attrs, &block)
render RubyUI::NativeSelectIcon.new
end
end
private
def default_attrs
{
data: {
slot: "native-select",
size: @size,
ruby_ui__form_field_target: "input",
action: "change->ruby-ui--form-field#onChange invalid->ruby-ui--form-field#onInvalid"
},
class: [
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] outline-none select-none data-[size=sm]:h-8 dark:bg-input/30 dark:hover:bg-input/50",
"placeholder:text-muted-foreground",
"selection:bg-primary selection:text-primary-foreground",
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"disabled:pointer-events-none disabled:cursor-not-allowed",
"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
]
}
end
end
end
2
Add RubyUI::NativeSelectDocs to app/components/ruby_ui/native_select/native_select_docs.rb
# frozen_string_literal: true
class Views::Docs::NativeSelect < Views::Base
def view_template
component = "NativeSelect"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Native Select", description: "A styled native HTML select element with consistent design system integration.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Default", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
NativeSelectOption(value: "pineapple") { "Pineapple" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Groups", description: "Use NativeSelectGroup to organize options into categories.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a department" }
NativeSelectGroup(label: "Engineering") do
NativeSelectOption(value: "frontend") { "Frontend" }
NativeSelectOption(value: "backend") { "Backend" }
NativeSelectOption(value: "devops") { "DevOps" }
end
NativeSelectGroup(label: "Sales") do
NativeSelectOption(value: "account_executive") { "Account Executive" }
NativeSelectOption(value: "sales_development") { "Sales Development" }
end
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Disabled", description: "Add the disabled attribute to the NativeSelect component to disable the select.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(disabled: true) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Size", description: "Use size: :sm for a compact select. Classes passed to NativeSelect style the wrapper, so class: \"w-full\" stretches the whole field.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(size: :sm, class: "w-full") do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Invalid", description: "Use aria-invalid to show validation errors.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(aria: {invalid: "true"}) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
RUBY
end
Heading(level: 2) { "Native Select vs Select" }
div(class: "space-y-2 text-sm text-muted-foreground") do
p { "NativeSelect: Choose for native browser behavior, superior performance, or mobile-optimized dropdowns." }
p { "Select: Choose for custom styling, animations, or complex interactions." }
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
end
3
Add RubyUI::NativeSelectGroup to app/components/ruby_ui/native_select/native_select_group.rb
# frozen_string_literal: true
module RubyUI
class NativeSelectGroup < Base
def view_template(&)
optgroup(**attrs, &)
end
private
def default_attrs
{
data: {slot: "native-select-optgroup"},
class: "bg-[Canvas] text-[CanvasText]"
}
end
end
end
4
Add RubyUI::NativeSelectIcon to app/components/ruby_ui/native_select/native_select_icon.rb
# frozen_string_literal: true
module RubyUI
class NativeSelectIcon < Base
# Renders the chevron <svg> itself, as shadcn does. A block replaces the
# chevron; its content is wrapped in a <span> that takes the same classes.
def view_template(&block)
if block
span(**attrs, &block)
else
svg(
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
**attrs
) do |s|
s.path(d: "m6 9 6 6 6-6")
end
end
end
private
def default_attrs
{
aria: {hidden: "true"},
data: {slot: "native-select-icon"},
class: "text-muted-foreground pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 select-none"
}
end
end
end
5
Add RubyUI::NativeSelectOption to app/components/ruby_ui/native_select/native_select_option.rb
# frozen_string_literal: true
module RubyUI
class NativeSelectOption < Base
def view_template(&)
option(**attrs, &)
end
private
# System colors keep options readable in the browser's native picker,
# whatever the page's color scheme.
def default_attrs
{
data: {slot: "native-select-option"},
class: "bg-[Canvas] text-[CanvasText]"
}
end
end
end
Components
| Component | Built using | Source |
|---|---|---|
NativeSelect | Phlex | |
NativeSelectDocs | Phlex | |
NativeSelectGroup | Phlex | |
NativeSelectIcon | Phlex | |
NativeSelectOption | Phlex |