Build Native Autocomplete in Rails Forms with f.datalist (No JavaScript)

Note: f.datalist is coming in Rails 8.2. It’s merged to main but not yet released. You can view the pull request, point your Gemfile at the main branch to try it, or clone the demo app and run bin/rails server. If you’d rather not wait, the underlying datalist_tag has shipped since Rails 8.0; more on that below. Also worth a look: type-safe JSON attributes with has_json, another 8.2 addition.


There’s an annoying gap between “I want suggestions in this text box” and “I just pulled in a JavaScript dependency, a controller action, and a Stimulus controller.” The browser has had a native answer for years, the <datalist> element, and Rails 8.2 adds a FormBuilder helper for it.

To be clear, this isn’t a new capability. You could already render a datalist with datalist_tag (Rails 8.0), or with plain HTML before that. What f.datalist adds is the id wiring, so the input and its list can’t fall out of sync. Small thing, but it’s a bug I’ve actually shipped, and the fix is two lines instead of three.

We’ll build a sign-up form with a country field that suggests matches as you type. No JS library, no fetch, no client state.

What We’re Building

A Profile form with a country input backed by a native suggestion list. The browser handles the typeahead, and we never leave the form_with block.

The Rails Feature: f.datalist

f.datalist wraps the existing datalist_tag and derives the element id so the input and list stay connected:

# actionview/lib/action_view/helpers/form_options_helper.rb
def datalist(method, choices = nil, html_options = {})
  @template.datalist_tag(field_id(method, "datalist"), choices, html_options)
end

The key piece is field_id(method, "datalist"). A <datalist> only works when the input’s list attribute matches the datalist’s id. Here’s the same form without the helper:

<%# The id is a literal string you now type identically in two places %>
<%= f.text_field :country, list: "profile_country_datalist" %>
<%= datalist_tag "profile_country_datalist", Profile::COUNTRIES %>

That works, but the id is a hardcoded string in two places. Rename the field or the model and they drift apart, and autocomplete quietly breaks with no error. field_id builds the same id from the field name on both sides, so they always match.

datalist_tag has been in released Rails since 8.0, so the manual version works today with no edge Rails. f.datalist just drops the bookkeeping.

Building It

Start with a plain model:

class Profile < ApplicationRecord
  validates :country, presence: true
end

Then the form. The two lines that matter are the text_field and the datalist:

<%= form_with model: @profile do |f| %>
  <div>
    <%= f.label :country %>
    <%= f.text_field :country, list: f.field_id(:country, :datalist) %>
    <%= f.datalist :country, ["Argentina", "Brazil", "Canada", "Chile", "Japan"] %>
  </div>

  <%= f.submit %>
<% end %>

The input’s list: and the id inside f.datalist are both field_id(:country, :datalist), so the browser pairs them.

This renders to:

<input list="profile_country_datalist" type="text"
       name="profile[country]" id="profile_country" />
<datalist id="profile_country_datalist">
  <option value="Argentina">Argentina</option>
  <option value="Brazil">Brazil</option>
  <option value="Canada">Canada</option>
  <option value="Chile">Chile</option>
  <option value="Japan">Japan</option>
</datalist>

Seeing It Work

Type in the country field and the browser shows a filtered dropdown. “C” narrows to Canada and Chile, “Ca” to Canada. Pick one to fill the input, or ignore the list and type anything, since a datalist suggests values rather than restricting them.

Nothing hit the server and no JavaScript ran. The browser handled the filtering on its own.

There’s no screenshot because the dropdown is drawn by the operating system, not the page, so it won’t appear in captures. Boot the demo app to see it for real.

Populating Choices from the Database

Hardcoded arrays are fine for a demo. The real use is feeding suggestions from your data. The choices argument takes the same format as options_for_select, so a query result drops in:

<%= f.text_field :tag, list: f.field_id(:tag, :datalist) %>
<%= f.datalist :tag, Tag.where.not(name: [nil, ""]).distinct.order(:name).pluck(:name) %>

The distinct and blank rejection keep duplicates and empty strings out of the list. Move that query into a scope or model method in real code. And since the field is a plain text_field, editing an existing record needs nothing extra: it prefills with the saved value and still offers the list.

The [label, value] form also works, like a select:

<%= f.datalist :country_code,
      [["Argentina", "AR"], ["Brazil", "BR"], ["Canada", "CA"]] %>

But a <datalist> is not a <select>. That renders <option value="AR">Argentina</option>, and a datalist matches your typing against the value (“AR”), inserts the value, and shows the value as the suggestion. The text (“Argentina”) is a secondary hint at best, and some browsers ignore it. The “show the name, submit the code” behavior you get from a select doesn’t carry over here, and typing “Arg” won’t find “Argentina”. Put what you want users to type into the value, which usually means the plain array form is what you want.

Taking It Further

A few directions worth exploring:

  • Multiple lists on one form. Each id comes from its own method name, so f.datalist :country and f.datalist :city never collide. Add as many as you like.
  • Per-option HTML. The format accepts a trailing options hash, so ["Chile", "CL", { disabled: true }] renders a disabled option.
  • Shared lists. For several inputs that share one set, drop to the plain datalist_tag('shared_id', choices) and point each input’s list: at "shared_id".

Things to Watch Out For

A datalist only offers suggestions; it won’t stop anyone from typing something off-list. Whatever sits in the field is what reaches your controller. So keep validating server-side: validates :country, presence: true, plus an inclusion check if you accept only a known set. The datalist is for typing comfort; the model still owns correctness.

Here’s a caveat: don’t set autocomplete="off" on the input. It’s a common habit for killing the browser’s own autofill, but Safari and other WebKit browsers read it as “no suggestions” and hide the datalist. If the list shows in Chrome but not Safari, that’s almost always why. Leave autocomplete off the input, or set it to a real token, and the list comes back.

Another thing to watch out for: every option ships in the HTML, and there’s no server-side filtering. A few hundred entries is fine. “Every city in the country” or “all 40,000 users” means a bloated payload and a sluggish list on each keystroke. Use f.datalist for bounded sets you’re happy to send in full. Past that, you want a real autocomplete backed by an endpoint.

The dropdown has limits, too. You can’t style it, screen-reader support varies across browsers, and mobile behavior differs by device. For a known set of suggestions none of that matters much, but if you need styling, full accessibility, or remote filtering, that’s where a JavaScript combobox earns its keep.

Wrapping Up

Native autocomplete in a Rails form now takes two lines: a text_field with a list: attribute and an f.datalist that derives the matching id. For the common “suggest from a known set” case, that’s the whole job. See PR #57318 for the implementation.