Tutorials 3 min read

How to create a search input with Tailwind CSS: JavaScript and Alpine.js

Build a live search input with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Filter a list of items as the user types, no extra libraries.

Today we are building a search input that filters a list of items as you type. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

What is a search input?

A search input lets users type a query and instantly retrieve matching results. Ours filters a small list as the user types, no search button needed, which is a quick way to help people find specific content without wading through everything.

Use cases

  • Product search: help shoppers find items in a large inventory.
  • File search: locate documents or images inside a directory tree.
  • Docs and knowledge bases: jump straight to the right article.
  • Data tables: narrow down large datasets to the rows that matter.

The markup

  • id="search-component": the container for the search input and the list of items.
  • id="search-input": the search input.
  • id="items-list": the list of items that gets filtered based on the search query.

Classes are omitted for brevity

html
<div id="search-component">
  <!-- Search Input -->
  <input id="search-input" />
  <!-- Filtered Items -->
  <ul id="items-list">
    <!-- List items will be injected here by JavaScript -->
  </ul>
</div>

The script

  • document.addEventListener("DOMContentLoaded", function () {: waits for the DOM, then grabs the component, the input, and the list.
  • const items = [...]: the array of items we filter, a handful of city names in this case.
  • function renderItems(filter = "") {: clears the list, filters the items with a case-insensitive includes, and appends a fresh li for each match.
  • searchInput.addEventListener("input", function () {: re-renders the list on every keystroke with the current query.
  • renderItems();: runs once at the start so the full list shows before any typing.
js
document.addEventListener("DOMContentLoaded", function () {
  const searchComponent = document.getElementById("search-component");
  const searchInput = document.getElementById("search-input");
  const itemsList = document.getElementById("items-list");

  const items = [
    "Milano",
    "Alicante",
    "Switzerland",
    "Bilbao",
    "Åland Islands",
    "Stockholm",
    "Torrevieja",
    "Minneapolis",
  ];

  function renderItems(filter = "") {
    itemsList.innerHTML = "";
    const filteredItems = items.filter((item) =>
      item.toLowerCase().includes(filter.toLowerCase())
    );
    filteredItems.forEach((item) => {
      const li = document.createElement("li");
      li.textContent = item;
      itemsList.appendChild(li);
    });
  }

  searchInput.addEventListener("input", function () {
    renderItems(searchInput.value);
  });

  // Initial render
  renderItems();
});

The Alpine.js version

Same component, no separate script: the state lives in x-data.

  • x-data="{ searchQuery: '', items: [...] }": stores the search query and the list of items, mapped into objects with a name.
  • <input type="text" x-model="searchQuery" placeholder="Search...">: binds the input to the query.
  • <template x-for="item in items" :key="item.name">: renders each item in the list.
  • x-show="item.name.toLowerCase().includes(searchQuery.toLowerCase())": hides the items that don’t match the query, and x-text="item.name" fills in the label.
html
<div
  x-data="{
        searchQuery: '',
        items: [
            'Milano', 'Alicante', 'Switzerland', 'Bilbao', 'Åland Islands', 'Stockholm', 'Torrevieja', 'Minneapolis'
        ].map(item => ({ name: item }))
    }"
>
  <!-- Search Input -->
  <input type="text" x-model="searchQuery" placeholder="Search..." />

  <!-- Filtered Items -->
  <ul>
    <template x-for="item in items" :key="item.name">
      <li
        x-show="item.name.toLowerCase().includes(searchQuery.toLowerCase())"
        x-text="item.name"
      ></li>
    </template>
  </ul>
</div>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

The Alpine version filters with x-show right in the template, a natural fit if Alpine is already around. The vanilla version re-renders the list itself and needs nothing but the browser.

Conclusion

This is a super simple search that filters a list without any extra libraries, not production-ready, but a great way to learn. Remember to add accessibility features like a label for the input before shipping it.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza