Senren UI
Components / Native Select

Native Select

Stable

short option lists

Usage example #

Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.

app/views/.../native_select_example.html.erb
<div class="w-full max-w-sm text-left">
  <%= render Senren::NativeSelectComponent.new(name: "demo_select", id: "demo_select", options: [["admin","Admin"],["member","Member"],["viewer","Viewer"]], prompt: "Choose a role") %>
</div>

Install this component #

Copy the official component into your app

Use this when you want the Senren-maintained implementation copied into app/components/senren.

Terminal
bin/rails senren:add native_select

Create a custom component with the same conventions

Use this when you need an app-specific static component that follows Senren's ViewComponent structure.

Terminal
bin/rails generate senren:component native_select --no-client

Dependencies are resolved by senren:add: label.

At a glance #

Category Forms
Class name Senren::NativeSelectComponent
Stimulus
Variants default, error
Depends on label
Pairs with form, label

Source #

app/components/senren/native_select_component.html.erb
<%= tag.div(**wrapper_attrs) do %>
  <%= tag.select(**select_attrs) do %>
    <% if prompt %>
      <option value=""><%= prompt %></option>
    <% end %>
    <% options.each do |opt| %>
      <% value, label = Array === opt ? opt : [opt, opt] %>
      <option value="<%= value %>" <%= "selected" if selected.to_s == value.to_s %>><%= label %></option>
    <% end %>
  <% end %>
  <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[hsl(var(--senren-muted-foreground))] transition-transform duration-150 group-focus-within:rotate-180">
    <path d="m5 8 5 5 5-5"></path>
  </svg>
<% end %>
app/components/senren/native_select_component.rb
# frozen_string_literal: true

module Senren
  class NativeSelectComponent < BaseComponent
    VARIANTS = {
      default: 'border-[hsl(var(--senren-input))] focus-visible:ring-[hsl(var(--senren-ring))]',
      error: 'border-[hsl(var(--senren-destructive))] focus-visible:ring-[hsl(var(--senren-destructive))]'
    }.freeze

    SIZES = {
      sm: 'h-8  text-sm  px-2.5',
      md: 'h-10 text-sm  px-3',
      lg: 'h-12 text-base px-4'
    }.freeze

    def initialize(name:, options:, selected: nil, id: nil, prompt: nil, variant: :default, size: :md, class_name: nil,
                   **html)
      super(variant: variant, size: size, class_name: class_name, **html)
      @name = name
      @options = options
      @selected = selected
      @id = id || name.to_s.parameterize
      @prompt = prompt
    end

    attr_reader :name, :options, :selected, :id, :prompt

    def wrapper_attrs
      { class: 'group relative w-full', data: { senren_component: senren_component_name } }
    end

    def select_attrs
      attrs = html_attrs.except(:class)
      attrs.merge(
        id: id,
        name: name,
        class: select_classes,
        'aria-invalid': variant == :error
      )
    end

    def select_classes
      [
        'flex w-full cursor-pointer appearance-none rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] pr-9 text-[hsl(var(--senren-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50',
        self.class::VARIANTS[variant],
        self.class::SIZES[size],
        class_name,
        html_attrs[:class]
      ].compact.join(' ')
    end
  end
end

AI agent rules #

Use for

  • +short option lists
  • +mobile-friendly selects

Avoid

  • -long lists - prefer combobox

Accessibility #

  • Pair with label; native option text must be meaningful.