This is HotwireCombobox

HotwireCombobox is at an early stage of development. It's nearing a beta release, but the API might change and bugs are expected. Please continue to use the library and report any issues in the GitHub repo.

It's a combobox (aka autocomplete) gem for Ruby on Rails applications.

You will need Turbo and Stimulus installed and running in your app to use this gem.

Head to our GitHub repo for installation instructions or read on for a quick demo.

HotwireCombobox prides itself on being easy to use. Let's dive into an implementation many people would think is complicated but this library makes simple.

YouTube-style autocomplete

    We'll get to a simpler example soon 😄. It's okay if you don't understand everything yet. You will. But for now, know that this is as complicated as it will get (which is not a lot!).

    This search input is an async combobox. Try it out!

    It's fetching its options from the server by hitting searchables_path and expecting a Turbo Stream template in return. If you scroll down far enough, you'll see the options are loaded in batches.

    Below is all the code you need to create one of these.

    In your template
    1
    2
    3
    4
    
    <%= form_with url: search_url do |form| %>
      <%= form.combobox :search, searchables_path,
            name_when_new: :search, mobile_at: "0px" %>
    <% end %>
    
    In views/searchables/index.turbo_stream.erb
    1
    2
    3
    
    <%= async_combobox_options @page.records,
          render_in: { partial: "searchables/searchable" },
          next_page: @page.last? ? nil : @page.next_param %>
    

    That's it!

    Now, before I explain all the options you can pass to the combobox, let's take a look at that simpler example I promised.

    A basic combobox

      This is a basic combobox. As plain as can be. Click on it to view all the options. Start typing to filter them down. Here's the code for it:

      1
      
      <%= combobox_tag "state", State.all %>
      

      The first param corresponds to the input's name. It's what the query param will be called when the form is submitted.

      The second param is the collection of options. Each option has a display (what the user sees in the combobox) and a value (what gets submitted with the form). This param can be one of multiple things:

      • ActiveRecord::Relation: the display is obtained by calling #to_combobox_display on each object and the value is obtained by calling #id.
      • url/path helper: options are requested asynchronously from this url/path. This is the only way to do server-side filtering. For all other setups, filtering is done client-side by the library. Read on for an example of an async implementation.
      • Single hash: object is iterated with #map. For each entry, options are determined as { display: option.first, value: option.last }
      • Array of strings: each entry is used as both display and value.
      • Array of arrays: For each entry, options are determined as { display: option.first, value: option.last }
      • Array of hashes: the display and value are provided as key-value pairs in each hash. e.g. { display: :Foo, value: :id_123 }.

      Values, displays, and even how autocompletion and filtering work can be customized. Read the source code for this kind of advanced usage.

      An async combobox

        Async comboboxes are useful because no time is spent loading all the options before the page is rendered. Instead, options are fetched when the combobox is first opened, and re-fetched as the user types in the combobox. You might reach for one of these when you're filtering a large dataset.

        Async comboboxes allow you to do the filtering server-side. Whenever the user types into the input, the combobox will perform a debounced request to your server to the url or path you provided. The user's input will be sent as a param accessible via params[:q].

        You'll provide the combobox options using the gem's #async_combobox_options helper. This needs to happen either in a *.turbo_stream.erb template, or directly inside the controller.

        Here's what that might look like:

        In your template
        1
        
        <%= combobox_tag "state", states_path, id: "state-box" %>
        
        In your controller
        1
        2
        3
        4
        5
        
        class StatesController < ApplicationController
          def index
            @states = State.search params[:q]
          end
        end
        
        In views/states/index.turbo_stream.erb
        1
        
        <%= async_combobox_options @states %>
        

        Or, you can return the options directly from the controller:

        In your controller, no need for a template
        1
        2
        3
        4
        5
        6
        
        class StatesController < ApplicationController
          def index
            @states = State.search params[:q]
            render turbo_stream: helpers.async_combobox_options @states
          end
        end
        

        You can optionally paginate the options. Simply pass the next page number as :next_page to #async_combobox_options. When the user scrolls down far enough, the combobox will automatically request that page number in a param accessible via params[:page] to the same url or path you provided.

        It's very important that you pass nil to :next_page for the last page. Otherwise, the combobox will keep requesting more pages indefinitely and will overwhelm your server.

        In views/states/index.turbo_stream.erb
        1
        2
        3
        
        <%# This is using geared_pagination syntax %>
        <%= async_combobox_options @page.records,
              next_page: @page.last? ? nil : @page.next_param %>
        

        An HTML combobox

          You can pass a full set of rendering options to the combobox. It'll use those options to render each item in the list.

          In your template
          1
          2
          
          <%= combobox_tag "state", State.all,
                render_in: { partial: "states/state" } %>
          

          As you saw from the YouTube example, :render_in is also supported in async comboboxes. Simply include the rendering options in the Turbo Stream template.

          In views/states/index.turbo_stream.erb
          1
          2
          3
          
          <%= async_combobox_options @page.records,
                render_in: { partial: "states/state" },
                next_page: @page.last? ? nil : @page.next_param %>
          

          An enum combobox

            Enums are supported out of the box. This is because the library supports passing a hash as the second argument. Here's an example for a rating enum that can be any of %w[ G PG PG-13 R NC-17 ]

            In your template
            1
            
            <%= combobox_tag "rating", Movie.ratings %>
            

            If you want more customization (maybe you want to titlecase the options), you can leverage the library's ability to accept a hash, an array, or an array of arrays as the second argument.

            A mobile combobox

              If you're reading this on a mobile device, this may not come as a surprise. But these comboboxes behave differently on desktop and mobile.

              On mobile, the options will appear inside a native HTML dialog element. You can style this however you want. But, by default, the options list will take up the full width of the screen and almost the full height. This makes the options easier to tap on smaller devices.

              You can configure the breakpoint at which the combobox switches to mobile mode by passing a :mobile_at option, which takes any valid CSS width value. Try resizing your browser window and opening the combobox above!

              1
              
              <%= combobox_tag "state", State.all, mobile_at: "640px" %>
              

              A prefilled combobox

                You can prefill a combobox with a value.

                This is most commonly done when using the form builder to edit a record. The library knows how to use the method name (in this example :favorite_movie_id) to find the value and prefill the input. The dropdown option will be preselected as well.

                1
                2
                3
                
                <%= form_with model: @user, url: user_url(@user) do |form| %>
                  <%= form.combobox :favorite_movie_id, Movie.all %>
                <% end %>
                

                When the combobox is async, the library can infer associations based on the presence of the _id suffix. If an association is found, the associated object is loaded and used to prefill the input. You can also provide the association name yourself via the :association_name option.

                When not using the form builder, you can pass a :value option to the combobox. It will be prefilled if an option is found with the same value.

                A free-text combobox

                  Sometimes you want to allow entering a value that isn't in the list of options. This is called a free-text combobox.

                  In most cases this is because you want your combobox to have a dual-purpose. On one hand, provide an existing id as a value for an association. On the other hand, allow the user to enter a new value that will be used to create a new record. You're essentially using a single combobox as two different form fields!

                  Luckily, the library supports that. It knows whether the input value is in the list of options. If it's not, it will use the :name_when_new option to set a new name for the form field. If you switch back to an existing option, the name will be reset to the original.

                  It's the presence of :name_when_new that tells the combobox it should allow free text. If it's not present, then free text is not allowed.

                  Note that there's nothing stopping you from using the same name for both the combobox and the :name_when_new opion, like in the YouTube example.

                  One way you might use this is in conjunction with Rails's accepts_nested_attributes_for feature.

                  1
                  2
                  3
                  4
                  
                  <%= form_with model: @user, url: user_url(@user) do |form| %>
                    <%= form.combobox :favorite_movie_id, Movie.all,
                          name_when_new: "user[favorite_movie_attributes][name]" %>
                  <% end %>
                  

                  Styling

                    We've come full circle to the original YouTube-style combobox. But this time, let's talk about styling.

                    You can opt-in to the default styles by including the gem's CSS file in your document's head via #combobox_style_tag . You can pass any of the usual #stylesheet_link_tag options to this helper.

                    Because there are many things you need to style, it is highly recommended you use the default styles as a starting point. Regardless of whether you've included the default styles, you always have full control over the combobox's appearance. Simply override some of these classes in your CSS:

                    • .hw-combobox: the fieldset container, wraps the whole component
                    • .hw-combobox--multiple: applied to the fieldset container on multiselect comboboxes
                    • .hw-combobox__main__wrapper: wraps the input (combobox) and options list (listbox)
                    • .hw-combobox__input: the field where the user inputs text
                    • .hw-combobox__input[data-queried]: applied to inputs which contain text
                    • .hw-combobox__input--invalid: applied to the input when it's invalid
                    • .hw-combobox__label: the main label for the combobox
                    • .hw-combobox__handle: the chevron or downwards arrow
                    • .hw-combobox__listbox: the collapsible options container
                    • .hw-combobox__option: each individual option
                    • .hw-combobox__option--selected: the currently selected option
                    • .hw-combobox__option--blank: applied to the blank option, if it exists
                    • .hw-combobox__group: applied to the <ul> element wrapping an option group
                    • .hw-combobox__group__label: the label at the top of each option group
                    • .hw-combobox__chip: the chip used to represent selected options in a multiselect combobox
                    • .hw-combobox__chip__remover: the element used to remove a multiselect chip
                    • .hw-combobox__dialog: contains the input, listbox, and options when on mobile mode
                    • .hw-combobox__dialog__wrapper: wraps the dialog used on mobile mode
                    • .hw-combobox__dialog__label: optional label displayed on mobile mode
                    • .hw-combobox__dialog__input: mobile mode input
                    • .hw-combobox__dialog__listbox: mobile mode listbox

                    You can also override the CSS variables defined by the library. Look in the source code for the full list of variables.

                    Here's one way you could style the combobox to look like the YouTube example using Tailwind (although you could use any CSS framework or vanilla CSS):

                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    
                    .youtube {
                      .hw-combobox__main__wrapper {
                        @apply sm:w-[35rem] rounded-full w-80;
                      }
                      .hw-combobox__handle {
                        @apply right-3;
                      }
                      .hw-combobox__listbox {
                        @apply rounded-2xl;
                      }
                      /* Magnifying glass icon: */
                      --hw-handle-image: url("data:image/svg+xml;charset=utf-8,/* encoded magnifying glass svg */");
                    }
                    

                    Other options

                    Here's a quick list of other options you can pass to the combobox:

                    • :autocomplete — choose one of :list, :inline, or :both, defaults to :both
                    • :dialog_label — text for the optional label displayed on mobile mode, defaults to nil
                    • :input — HTML options to be forwarded to the input, defaults to {}
                    • :open — whether the combobox should be open on first render, defaults to false

                    Any other unrecognized options are forwarded to the input element.