Lexington has been awarded a grant from Astro, to celebrate. Get a 30% discount. Apply code LEXINGTON30 ( uppercase ) at checkout.

← Back to all tutorials

How to create a combo box with Tailwind CSS and Javascript

js-combo-box
Published and written on Sep 13 2024 by Michael Andreuzza

Today we’ll be creating a basic combo box using Tailwind CSS and JavaScript. Super simple, without functionality, but it’s a great starting point for building more complex combo boxes that can be used afterwards.

What is a combo box?

A combo box is a user interface element that allows users to quickly access and execute various commands or actions within an application. It typically appears as a search-like input field that, when activated, displays a list of available commands or options. Users can then search for and select the desired action using keyboard navigation or mouse clicks. combo boxes enhance productivity by providing a fast and efficient way to interact with an application’s features.

Use cases

combo boxes have numerous applications across different types of software:

  1. Text editors and IDEs: Quickly access commands, search for files, and navigate between different parts of a project.

  2. Operating systems: Rapidly launch applications and control system functions (e.g., Spotlight on macOS, Start menu search on Windows).

  3. Productivity apps: Create tasks, search for notes, and perform quick actions in tools like Notion, Todoist, or Evernote.

  4. Web applications: Navigate between different sections of a complex web app, such as in project management tools or customer relationship management (CRM) systems.

  5. Design tools: Access tools, layers, and commands swiftly in software like Figma, Sketch, or Adobe Creative Suite.

  6. Browser extensions: Execute various actions without leaving the current webpage, like translation, note-taking, or sharing.

  7. Enterprise software: Navigate large, complex systems and perform frequent actions more efficiently.

  8. Content management systems (CMS): Quickly find and edit content, change settings, or manage users in platforms like WordPress or Drupal.

  9. Data analysis tools: Run queries, change visualizations, or access different datasets in business intelligence software.

  10. Game development engines: Access different tools, assets, or settings in environments like Unity or Unreal Engine.

By implementing a combo box, you can significantly improve the user experience and efficiency of your application, especially for power users who prefer keyboard-driven interfaces.

Let’s start writing the code

The input field

The input field is the core component of the combo box, where users can type in their search query or select a command from the dropdown menu. We’ll use the input element to create the input field. Our input will include a placeholder text and a role attribute to indicate that it’s a combobox.

The wrapper for the input field

Id’s

  • id="combo-box": This is the id of the wrapper element. Classes
  • relative: This is a class that sets the position of the wrapper element to relative.
The SVG search icons
  • absolute: This is a class that sets the position of the SVG search icons to absolute.
  • top-3.5: This is a class that sets the top position of the SVG search icons to 3.5.
  • left-4: This is a class that sets the left position of the SVG search icons to 4.
  • size-5: This is a class that sets the size of the SVG search icons to 5.
The input field

Type

  • type="text": This is the type of the input field. Id’s
  • id="combo-input": This is the id of the input field. Attributes
  • role="combobox": This is an attribute that indicates that the input field is a combobox.
  • aria-expanded="false": This is an attribute that indicates that the combobox is not expanded.
  • aria-autocomplete="list": This is an attribute that indicates that the combobox has a list of suggestions.
  • aria-activedescendant="": This is an attribute that indicates that the combobox has no active descendant.
  • aria-controls="combo-menu": This is an attribute that indicates that the combobox controls the combo box menu.
  • placeholder="Search for anything": This is an attribute that sets the placeholder text of the input field. Classes
  • w-full: This is a class that sets the width of the input field to 100% of its parent container.
  • h-12: This is a class that sets the height of the input field to 12 pixels.
  • pl-11: This is a class that sets the left padding of the input field to 11 pixels.
  • pr-4: This is a class that sets the right padding of the input field to 4 pixels. only relevant classes are added to see the final result checkout the live demo.
<div
   id="combo-box"
   class="relative">
   <svg class="absolute top-3.5 left-4 size-5">
      <!-- SVG search goes here>
   </svg>
   <input
      type="text"
      role="combobox"
      id="combo-input"
      aria-expanded="false"
      aria-autocomplete="list"
      aria-activedescendant=""
      aria-controls="combo-menu"
      placeholder="Search for anything"
      class="w-full h-12 pl-11 pr-4"
      />
</div>
The combo box menu

The combo box menu is the dropdown menu that appears when the user clicks on the input field. Id’s

  • id="combo-menu": This is the id of the combo box menu. Roles
  • role="listbox": This is an attribute that indicates that the combo box menu is a listbox.
  • aria-label="combo box options": This is an attribute that sets the label of the combo box menu to “combo box options”. Classes
  • hidden: This is a class that sets the visibility of the combo box menu to hidden.
  • overflow-hidden: This is a class that sets the overflow of the combo box menu to hidden.
<ul
   id="combo-menu"
   role="listbox"
   aria-label="combo box options"
   class="hidden  overflow-hidden">
   <!-- combo box options go here --->
</ul>
The combo box options

The combo box options are the list items that appear in the combo box menu when the user clicks on the input field. Id’s

  • id="option-1": This is the id of the first command option. This option will be used to reference the command option in JavaScript. Roles
  • option: This is an attribute that indicates that the command option is an option.
  • aria-selected="false": This is an attribute that indicates that the command option is not selected.
<li
   id="option-1"
   role="option"
   aria-selected="false">
   <svg>
      <!-- SVG icons go here -->
   </svg>
   Search projects
</li>
<div>
   <div
   id="combo-box"
   class="relative">
   <svg class="absolute top-3.5 left-4 size-5">
      <!-- SVG search goes here>
   </svg>
   <input
      type="text"
      role="combobox"
      id="combo-input"
      aria-expanded="false"
      aria-autocomplete="list"
      aria-activedescendant=""
      aria-controls="combo-menu"
      placeholder="Search for anything"
      class="w-full h-12 pl-11 pr-4"
      />
</div>
   <ul
      id="combo-menu"
      class="hidden mt-2 overflow-hidden bg-white rounded-lg shadow-xl border border-neutral-200"
      role="listbox"
      aria-label="combo box options">
      <li
         id="option-1"
         class="flex items-center p-3 gap-4 text-sm text-neutral-600 cursor-pointer hover:bg-neutral-50 hover:text-blue-500"
         role="option"
         aria-selected="false">
         <svg
            xmlns="http://www.w3.org/2000/svg"
            class="size-4"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            aria-hidden="true">
            <path d="M3 12l3 3l3 -3l-3 -3z"></path>
            <path d="M15 12l3 3l3 -3l-3 -3z"></path>
            <path d="M9 6l3 3l3 -3l-3 -3z"></path>
            <path d="M9 18l3 3l3 -3l-3 -3z"></path>
         </svg>
         Search projects
      </li>
      <!-- combo box options go here -->
   </ul>
</div>

Let’s write the JavaScript code

The variables

We’ll use the input element to create the input field. We’ll use the menu element to get the combo box menu.

  • const input = document.getElementById("combo-input");: This line of code gets the input field element.
  • const menu = document.getElementById("combo-menu");: This line of code gets the combo box menu element.
  • const options = menu.querySelectorAll('[role="option"]');: This line of code gets all the combo box options in the combo box menu.
  • let currentFocus = -1;: This line of code declares a variable called currentFocus and sets its value to -1.
const input = document.getElementById("combo-input");
const menu = document.getElementById("combo-menu");
const options = menu.querySelectorAll('[role="option"]');
let currentFocus = -1;

Event listeners and initialization

The event listener for the input field
  • input.addEventListener("focus", () => {: This line of code adds an event listener to the input field.
  • menu.classList.remove("hidden");: This line of code removes the hidden class from the combo box menu.
  • input.setAttribute("aria-expanded", "true");: This line of code sets the aria-expanded attribute of the input field to true.
input.addEventListener("focus", () => {
   menu.classList.remove("hidden");
   input.setAttribute("aria-expanded", "true");
});
The blur event listener
  • input.addEventListener("blur", () => {: This line of code adds an event listener to the blur event of the input field.
  • setTimeout(() => {: This line of code sets a timeout of 100 milliseconds.
  • menu.classList.add("hidden");: This line of code adds the hidden class to the combo box menu.
  • input.setAttribute("aria-expanded", "false");: This line of code sets the aria-expanded attribute of the input field to false.
  • }, 100);: This line of code closes the timeout.
input.addEventListener("blur", () => {
   setTimeout(() => {
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
   }, 100);
});
The keydown event listener
  • input.addEventListener("keydown", (e) => {: This line of code adds an event listener to the keydown event of the input field.
  • if (e.key === "ArrowDown" || e.key === "ArrowUp") {: This line of code checks if the key pressed is either ArrowDown or ArrowUp.
  • e.preventDefault();: This line of code prevents the default behavior of the keydown event.
  • const direction = e.key === "ArrowDown" ? 1 : -1;: This line of code sets the direction variable to 1 if the key pressed is ArrowDown and -1 if the key pressed is ArrowUp.
  • currentFocus = (currentFocus + direction + options.length) % options.length;: This line of code calculates the new currentFocus value based on the direction and the length of the options.
  • updateFocus();: This line of code calls the updateFocus function.
  • } else if (e.key === "Enter" && currentFocus !== -1) {: This line of code checks if the key pressed is Enter and if the currentFocus is not -1.
  • e.preventDefault();: This line of code prevents the default behavior of the keydown event.
  • options[currentFocus].click();: This line of code clicks the current option.
  • } else if (e.key === "Escape") {: This line of code checks if the key pressed is Escape.
  • menu.classList.add("hidden");: This line of code adds the hidden class to the combo box menu.
  • input.setAttribute("aria-expanded", "false");: This line of code sets the aria-expanded attribute of the input field to false.
  • input.blur();: This line of code blurs the input field.
input.addEventListener("keydown", (e) => {
   if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault();
      const direction = e.key === "ArrowDown" ? 1 : -1;
      currentFocus =
         (currentFocus + direction + options.length) % options.length;
      updateFocus();
   } else if (e.key === "Enter" && currentFocus !== -1) {
      e.preventDefault();
      options[currentFocus].click();
   } else if (e.key === "Escape") {
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
      input.blur();
   }
});
The function to update the focus
  • function updateFocus() {: This line of code defines a function called updateFocus.
  • options.forEach((option, index) => {: This line of code iterates over each option in the options array.
  • if (index === currentFocus) {: This line of code checks if the current index is equal to the currentFocus.
  • option.classList.add("bg-blue-50");: This line of code adds the bg-blue-50 class to the option.
  • option.setAttribute("aria-selected", "true");: This line of code sets the aria-selected attribute of the option to true.
  • input.setAttribute("aria-activedescendant", option.id);: This line of code sets the aria-activedescendant attribute of the input field to the id of the option.
  • } else {: This line of code closes the if statement.
function updateFocus() {
   options.forEach((option, index) => {
      if (index === currentFocus) {
         option.classList.add("bg-blue-50");
         option.setAttribute("aria-selected", "true");
         input.setAttribute("aria-activedescendant", option.id);
      } else {
         option.classList.remove("bg-blue-50");
         option.setAttribute("aria-selected", "false");
      }
   });
}
The initialization
  • options.forEach((option) => {: This line of code iterates over each option in the options array.
  • option.addEventListener("click", () => {: This line of code adds an event listener to the click event of the option.
  • input.value = option.textContent.trim();: This line of code sets the value of the input field to the text content of the option.
  • menu.classList.add("hidden");: This line of code adds the hidden class to the combo box menu.
  • input.setAttribute("aria-expanded", "false");: This line of code sets the aria-expanded attribute of the input field to false.
  • input.focus();: This line of code focuses the input field.
  • });: This line of code closes the event listener.
  • option.addEventListener("mouseenter", () => {: This line of code adds an event listener to the mouseenter event of the option.
  • currentFocus = Array.from(options).indexOf(option);: This line of code sets the currentFocus variable to the index of the option in the options array.
  • updateFocus();: This line of code calls the updateFocus function.
options.forEach((option) => {
   option.addEventListener("click", () => {
      input.value = option.textContent.trim();
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
      input.focus();
   });

   option.addEventListener("mouseenter", () => {
      currentFocus = Array.from(options).indexOf(option);
      updateFocus();
   });
});

The complete script

const input = document.getElementById("combo-input");
const menu = document.getElementById("combo-menu");
const options = menu.querySelectorAll('[role="option"]');
let currentFocus = -1;

input.addEventListener("focus", () => {
   menu.classList.remove("hidden");
   input.setAttribute("aria-expanded", "true");
});

input.addEventListener("blur", () => {
   setTimeout(() => {
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
   }, 100);
});

input.addEventListener("keydown", (e) => {
   if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault();
      const direction = e.key === "ArrowDown" ? 1 : -1;
      currentFocus =
         (currentFocus + direction + options.length) % options.length;
      updateFocus();
   } else if (e.key === "Enter" && currentFocus !== -1) {
      e.preventDefault();
      options[currentFocus].click();
   } else if (e.key === "Escape") {
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
      input.blur();
   }
});

function updateFocus() {
   options.forEach((option, index) => {
      if (index === currentFocus) {
         option.classList.add("bg-blue-50");
         option.setAttribute("aria-selected", "true");
         input.setAttribute("aria-activedescendant", option.id);
      } else {
         option.classList.remove("bg-blue-50");
         option.setAttribute("aria-selected", "false");
      }
   });
}

options.forEach((option) => {
   option.addEventListener("click", () => {
      input.value = option.textContent.trim();
      menu.classList.add("hidden");
      input.setAttribute("aria-expanded", "false");
      input.focus();
   });

   option.addEventListener("mouseenter", () => {
      currentFocus = Array.from(options).indexOf(option);
      updateFocus();
   });
});

Conclusion

In this tutorial, we’ve walked through the process of creating a functional combo box using Tailwind CSS and JavaScript. We’ve covered the essential HTML structure, styling with Tailwind CSS, and the JavaScript logic needed to make the combo box interactive and accessible.

This combo box implementation offers several benefits:

  1. Enhanced user experience with keyboard navigation and mouse interaction
  2. Improved accessibility through proper ARIA attributes
  3. Clean, responsive design using Tailwind CSS
  4. Customizable and extendable foundation for more complex applications

By following this tutorial, you’ve gained valuable insights into building interactive UI components that combine form and function. As you continue to develop your skills, consider expanding on this combo box by adding features like live search filtering, custom styling, or integration with backend data sources.

Remember, the combo box is just one example of how you can create powerful, user-friendly interfaces. Use the principles and techniques you’ve learned here as a springboard for creating other interactive components and elevating your web development projects.

/Michael Andreuzza

Did you like this tutorial? Please share it with your friends!

Get lifetime access to every theme available today for $199 and own them forever. Plus, new themes, lifetime updates, use on unlimited projects and enjoy lifetime support.

— No subscription required!