Low commit activity in last 3 years
A long-lived project that still receives updates
Country State Select is a Ruby Gem that aims to make Country and State/Province selection a cinch in Ruby on Rails environments.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

Country State Select

CI Code Climate

Country State Select gives you cascading Country → State/Province → City dropdowns for Rails forms: pick a country, the state field repopulates; pick a state, the city field repopulates.

Cascading country, state, and city selects

Country/state/state data comes from the city-state gem (pluggable — see Custom data sources).

v4.0 is a modernization release. Highlights:

  • Dependency-free JavaScript (no jQuery) with a bundled Stimulus controller
  • Works with importmap-rails, Sprockets, Propshaft, or npm/esbuild/webpack
  • Native form_with/form_for support (f.country_select, f.state_select, f.city_select) — simple_form is no longer required
  • priority_countries, only/except filtering, flags, dial codes, I18n-localized names
  • A pluggable data source adapter
  • HTTP-cached JSON lookup endpoints
  • An install generator (rails g country_state_select:install)

Upgrading from 3.x? See docs/migration-v4.md — the legacy jQuery/Chosen JavaScript still works, but Rails < 7.0 is no longer supported (stay on the 3.x gem for older Rails).

Installation

gem 'country_state_select'
bundle install
rails g country_state_select:install   # optional: initializer + setup instructions

Requires Rails >= 7.0 and Ruby >= 3.1.

JavaScript setup

Pick the one that matches your app. All of them talk to the same /find_states and /find_cities JSON endpoints (see docs/json-api.md).

Importmap + Stimulus (Rails 7/8 default)

The engine registers its pins automatically — no pin lines to write yourself. In app/javascript/application.js:

import "country_state_select"
import { Application } from "@hotwired/stimulus"
import CountryStateSelectController from "country_state_select/controller"

const application = Application.start()
application.register("country-state-select", CountryStateSelectController)

Or, with stimulus-rails' app/javascript/controllers/index.js auto-loading convention, just re-export it as its own controller file:

// app/javascript/controllers/country_state_select_controller.js
export { default } from "country_state_select/controller"

Then in a view, wrap your fields in a controller container:

<div data-controller="country-state-select">
  <%= f.country_select :country %>
  <%= f.state_select :state, :country %>
  <%= f.city_select :city, :state, :country %>
</div>

Stimulus controller values (all optional, set as data-country-state-select-<value>-value="..."):

Value Default Purpose
states-url /find_states Override if you disabled draw_routes and mounted the engine elsewhere
cities-url /find_cities Same, for cities
tom-select false Enhance the selects with Tom Select once populated (requires TomSelect to be loaded globally)
announce true Announce dropdown updates via an aria-live region
state-placeholder / city-placeholder "" Blank option text shown before a real selection

Sprockets (no jQuery)

//= require country_state_select_vanilla
document.addEventListener('DOMContentLoaded', function () {
  CountryStateSelect({ country_id: "country_field_id", state_id: "state_field_id" })
})

npm / esbuild / webpack

npm install country_state_select @hotwired/stimulus
import CountryStateSelectController from "country_state_select/controller"
// or, for the framework-agnostic core directly:
import CountryStateSelect from "country_state_select"

@hotwired/stimulus is an optional peer dependency — only needed if you import the /controller entry point.

Legacy jQuery + Chosen (deprecated)

Still bundled for apps upgrading from 3.x. New apps should use one of the options above.

//= require country_state_select
//= require chosen-jquery   // optional, if you want the Chosen UI
$(document).on('turbo:load', function () {
  CountryStateSelect({
    country_id: "country_field_id",
    state_id: "state_field_id",
    chosen_ui: true,
    chosen_options: { disable_search_threshold: 10 }
  })
})

Using it in a form

form_with (no simple_form required)

<%= form_with model: @business do |f| %>
  <div data-controller="country-state-select">
    <%= f.country_select :country, include_blank: true %>
    <%= f.state_select :state, :country %>
    <%= f.city_select :city, :state, :country %>
  </div>
<% end %>
  • state_select/city_select read the model's current country/state to render the right options server-side — on an edit form, the saved state and city show up correctly without waiting on JavaScript.
  • When the parent selection has no states/cities (e.g. a country with no states), the field renders as a plain text input, same as before.
  • only:, except:, and priority: options work on all three: f.country_select :country, priority: %w[US CA].

simple_form (unchanged from 3.x)

<%= simple_form_for @form_object do |f| %>
  <%= f.input :country_field, collection: CountryStateSelect.countries_collection %>

  <% options = { form: f, field_names: { country: :country_field, state: :state_field } } %>
  <%= f.input :state_field, CountryStateSelect.state_options(options) %>
<% end %>

City support: see docs/cities.md.

Configuration

# config/initializers/country_state_select.rb
CountryStateSelect.configure do |config|
  config.priority_countries = %w[US CA GB]   # shown first in the dropdown
  config.only_countries = %w[US CA GB IN]    # restrict the list
  config.except_countries = %w[KP]           # or exclude specific ones

  config.flags = true          # "🇮🇳 India"
  config.dial_codes = true     # "India (+91)"
  config.localize_names = true # translate names via I18n / the `countries` gem

  config.cache_duration = 1.day # HTTP cache on /find_states, /find_cities; nil disables it
  config.draw_routes = true     # set false to mount the engine at a custom path instead
end

Every option here can also be passed per-call: CountryStateSelect.countries_collection(priority: %w[US]), f.country_select :country, only: %w[US CA].

Custom data sources

Swap out city-state (e.g. for a database table, or the countries gem) by implementing the adapter interface:

class MyDataSource < CountryStateSelect::DataSources::Base
  def countries
    { US: 'United States', IN: 'India' } # code => English name
  end

  def states(country_code)
    [[:CA, 'California'], [:NY, 'New York']] # [code, name] pairs
  end

  def cities(state_code, country_code)
    ['Los Angeles', 'San Francisco'] # plain names
  end
end

CountryStateSelect.configure { |c| c.data_source = MyDataSource.new }

Custom route path

By default the engine draws /find_states and /find_cities at the top level. To mount it elsewhere:

# config/initializers/country_state_select.rb
CountryStateSelect.configure { |c| c.draw_routes = false }
# config/routes.rb
scope path: '/csc' do
  match 'find_states' => 'country_state_select/cscs#find_states', via: :get
  match 'find_cities' => 'country_state_select/cscs#find_cities', via: :get
end

Then pass the matching URLs to the JS: states_url: '/csc/find_states' (or the Stimulus states-url-value).

JSON API

The lookup endpoints are documented in docs/json-api.md — useful if you're driving the selects from a SPA or mobile client instead of the bundled JS.

Development

bundle install
bundle exec rspec

The dummy Rails app used for integration tests lives in spec/dummy and doubles as a runnable demo:

cd spec/dummy
bin/rails server

CI runs the suite across Ruby 3.1–3.4 (latest Rails) and Rails 7.0/7.1/7.2/8.0 (see gemfiles/).

Contributing

Fork, fix, then send a pull request.