0.0
The project is in a healthy, maintained state
A zero-dependency Rails form helper that renders a searchable dropdown whose options are fetched from a JSON endpoint. Supports pagination, dependent selects, keyboard navigation, full ARIA, i18n, and Turbo.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.0
 Project Readme

remote_select

A lightweight, zero-dependency Rails form helper that renders a searchable select whose options are fetched from a JSON endpoint.

Replaces Select2 / Tom Select for the common case of "type to search a remote list" — with no npm packages, no jQuery, and full Turbo support.

Features

  • Remote data fetching with debouncing and request cancellation (AbortController)
  • Keyboard navigation — ↑↓ arrows, Enter to select, Escape to close
  • Full ARIArole="combobox", aria-expanded, aria-activedescendant, role="listbox"
  • Pagination — infinite scroll loading of additional results
  • Dependent selects — pass values from other fields as query parameters automatically
  • Auto-clearing — clear selection when a dependency changes
  • Minimum character threshold — configurable before search fires
  • Preselected values — display preselected options on page load
  • i18n — default strings resolved via I18n.t with English fallbacks
  • Turbo compatible — full-page navigation and Turbo Frames, no Stimulus required
  • Select2-compatible — accepts Select2 pagination.more response format out of the box
  • No dependencies — pure vanilla JavaScript (ES2020)

Installation

Add to your Gemfile:

gem "remote_select"

Run:

bundle install

Then run the install generator to copy JS and CSS into your app:

rails generate remote_select:install

The generator copies two files and prints setup instructions for your specific asset pipeline (esbuild, importmap, Sprockets, etc.).

Use --force to overwrite previously copied files when updating the gem:

rails generate remote_select:install --force

Manual setup (if you prefer not to use the generator)

Importmap (Rails 8 default)

No generator needed — importmap resolves from the engine automatically.

# config/importmap.rb
pin "remote_select", to: "remote_select.js"
// app/javascript/application.js
import "remote_select"

For CSS, add to application.css:

/*= require remote_select */
Sprockets (no bundler)
// app/assets/javascripts/application.js
//= require remote_select
/* app/assets/stylesheets/application.css */
/*= require remote_select */
esbuild / rollup / webpack (jsbundling-rails)

Run the generator first (see above), then:

// app/javascript/application.js
import "./remote_select"

For CSS, add to your SCSS/CSS entry:

@import './remote_select';

Usage

Basic

<%= form_with model: @article do |form| %>
  <%= form.label :company_id %>
  <%= remote_select(form, :company_id, search_companies_path,
        placeholder: "Type to search companies...",
        min_chars: 2) %>
<% end %>

With preselected value

<%= remote_select(form, :company_id, search_companies_path,
      selected_value: @article.company_id,
      selected_text:  @article.company&.name) %>

Dependent select (clears when parent changes)

<%# Parent — standard Rails select %>
<%= form.select :source, sources_options, {}, { id: "source-selector" } %>

<%# Child — clears and re-fetches when source changes %>
<%= remote_select(form, :company_id, search_companies_path,
      depends_on: "#source-selector",
      clear_on_dependency_change: true) %>

Multiple dependencies

<%= remote_select(form, :city_id, search_cities_path,
      depends_on: "#state-selector, #country-selector") %>

Helper options

Option Type Default Description
endpoint String required JSON endpoint URL
selected_value String/Integer nil Pre-selected value ID
selected_text String nil Pre-selected display text
min_chars Integer 2 Chars needed before search fires
debounce_delay Integer 250 Debounce in milliseconds
placeholder String i18n Placeholder text
per_page Integer 20 Results per page
depends_on String nil CSS selector(s) of dependency field(s)
clear_on_dependency_change Boolean true Clear when dependency changes
empty_text String i18n Text shown when no results
loading_text String i18n Text shown while loading
html Hash {} Extra HTML attrs on the hidden input

i18n

Override any key in your locale files:

# config/locales/en.yml
en:
  remote_select:
    placeholder: "Type to search..."
    empty_text: "No results found"
    loading_text: "Loading..."

Backend endpoint

Your action must return JSON with a results array. Pagination is optional.

Recommended format

def search_companies
  query    = params[:q].to_s.strip
  page     = (params[:page] || 1).to_i
  per_page = (params[:per_page] || 20).to_i

  companies = Company.order(:name)
  companies = companies.where("name ILIKE ?", "%#{query}%") if query.present?

  total   = companies.count
  results = companies.limit(per_page).offset((page - 1) * per_page)

  render json: {
    results:  results.map { |c| { id: c.id, text: c.name } },
    has_more: (page * per_page) < total,
    total:    total
  }
end
{
  "results":  [{ "id": 1, "text": "Acme Corp" }],
  "has_more": true,
  "total":    150
}

Select2-compatible format (also accepted)

If you're migrating from Select2, your existing endpoints work as-is:

{
  "results":  [{ "id": 1, "text": "Acme Corp" }],
  "pagination": { "more": true }
}

Both has_more and pagination.more are recognized. If neither is present, pagination is disabled.

Theming

All visual properties are CSS custom properties on .remote-select-container. Override without touching the stylesheet:

.my-form .remote-select-container {
  --rs-focus-border-color: #198754;
  --rs-focus-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
  --rs-item-hover-bg: #d1e7dd;
  --rs-border-radius: 0.25rem;
  --rs-dropdown-max-height: 400px;
}

Available CSS custom properties

Property Default Description
--rs-text-color #212529 Main text color
--rs-muted-color #6c757d Placeholder / message color
--rs-bg-color #fff Background
--rs-border-color #dee2e6 Border color
--rs-border-radius 0.375rem Corner radius
--rs-focus-border-color #86b7fe Focus ring border
--rs-focus-shadow blue 0.25rem Focus ring shadow
--rs-item-hover-bg #e9ecef Hovered item background
--rs-error-color #dc3545 Error text color
--rs-zindex 1050 Dropdown z-index
--rs-dropdown-max-height 300px Dropdown max height
--rs-results-max-height 250px Results list max height

JavaScript API

// Manual init
const rs = new RemoteSelect(document.querySelector('#my-input'), {
  endpoint: '/api/search',
  minChars: 3
});

rs.setParam('category', 'books'); // add extra query param
rs.clearParams();                 // remove all extra params
rs.clearSelection();
rs.openDropdown();
rs.closeDropdown();
rs.destroy();                     // remove all listeners + DOM

Browser support

Modern browsers (Chrome, Firefox, Safari, Edge). Requires ES2020 (??, ?., async/await, AbortController). No IE11.

License

MIT