Tailwind Search Bar: 16 Examples With Autocomplete

Tailwind CSS v4 changed defaults that alter a search input without touching its markup. The bare ring shrank from 3px to 1px and the border now follows the text color, so snippets copied from v3 tutorials render with a thin, text-colored focus ring.

Below are 16 Tailwind search bar examples you can copy, followed by a guide to building, styling and wiring one up.

By Bogdan Sandu · Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Search field anatomy

The parts every search bar is made of: a search landmark, a labelled type="search" input, a leading icon, a clear button and a submit button. These demos show how Tailwind styles each part, including the native cancel button that WebKit and Blink draw inside search fields.

Search landmark with label and leading icon

  • <search>
  • type="search"
  • focus:ring-3
  • outline-hidden

Type a query and press Enter. Submitting is prevented, and a result line shows what the form would send.

The search element gives the form a search landmark, the same thing role="search" on the form does in older browsers. The label is visible and tied to the input, the icon sits in an absolute, pointer-events-none layer, and pl-10 keeps typed text clear of it. focus:ring-3 with an explicit teal color replaces the v3 ring default, and outline-hidden keeps an outline in forced colors mode.

View code
<search class="tsr-land block w-full max-w-lg">
  <form class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-stone-900/5 md:p-6">
    <label for="tsr-land-q" class="block text-base font-semibold text-stone-900">Search the help center</label>
    <p id="tsr-land-help" class="mt-1 text-sm text-stone-600">Try invoice, refund or export.</p>
    <div class="mt-4 flex gap-2">
      <div class="relative min-w-0 flex-1">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-stone-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
        <input id="tsr-land-q" name="q" type="search" placeholder="Search articles" aria-describedby="tsr-land-help" class="h-11 w-full rounded-lg border border-stone-300 bg-white pr-3 pl-10 text-base text-stone-900 placeholder:text-stone-500 focus:border-teal-700 focus:ring-3 focus:ring-teal-700/25 focus:outline-hidden">
      </div>
      <button type="submit" class="h-11 shrink-0 cursor-pointer rounded-lg bg-teal-700 px-4 text-sm font-semibold text-white hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Search</button>
    </div>
    <p role="status" class="tsr-land-out mt-3 min-h-5 text-sm text-stone-700"></p>
    <div class="mt-3 border-t border-stone-100 pt-4">
      <p class="text-sm font-semibold text-stone-900">Popular articles</p>
      <ul class="mt-2 space-y-1 text-sm">
        <li><a href="#" class="text-teal-800 underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-teal-700">Download a past invoice</a></li>
        <li><a href="#" class="text-teal-800 underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-teal-700">Change the card on file</a></li>
        <li><a href="#" class="text-teal-800 underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-teal-700">Export customers as CSV</a></li>
      </ul>
    </div>
  </form>
</search>

Native cancel button: default, restyled, hidden

  • [&::-webkit-search-cancel-button]:
  • appearance-none
  • @utility
  • clip-path

Hover or focus a field to see its clear button, then click it.

Chrome, Edge and Safari draw a clear icon inside type="search" fields, reachable through the ::-webkit-search-cancel-button pseudo-element. An arbitrary variant styles it in place: appearance-none removes the native glyph, size-4 and a fuchsia background redraw it, and a small @utility clips the square into an X. The third field hides the button entirely, which you want when the design has its own clear button. Firefox draws no native button, so all three look alike there.

View code
<div class="w-full max-w-md space-y-5 rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5 md:p-6">
  <div>
    <label for="tsr-nc-a" class="block text-sm font-semibold text-slate-900">Browser default</label>
    <input id="tsr-nc-a" type="search" value="quarterly report" class="mt-2 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-base text-slate-900 focus:border-fuchsia-600 focus:ring-3 focus:ring-fuchsia-600/20 focus:outline-hidden">
  </div>
  <div>
    <label for="tsr-nc-b" class="block text-sm font-semibold text-slate-900">Restyled with an arbitrary variant</label>
    <input id="tsr-nc-b" type="search" value="quarterly report" class="mt-2 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-base text-slate-900 focus:border-fuchsia-600 focus:ring-3 focus:ring-fuchsia-600/20 focus:outline-hidden [&::-webkit-search-cancel-button]:size-4 [&::-webkit-search-cancel-button]:cursor-pointer [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-cancel-button]:bg-fuchsia-600 [&::-webkit-search-cancel-button]:tsr-x [&::-webkit-search-cancel-button:hover]:bg-fuchsia-800">
  </div>
  <div>
    <label for="tsr-nc-c" class="block text-sm font-semibold text-slate-900">Hidden with appearance-none</label>
    <input id="tsr-nc-c" type="search" value="quarterly report" class="mt-2 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-base text-slate-900 focus:border-fuchsia-600 focus:ring-3 focus:ring-fuchsia-600/20 focus:outline-hidden [&::-webkit-search-cancel-button]:appearance-none">
    <p class="mt-2 text-sm text-slate-600">Escape still clears the field in Chrome and Edge.</p>
  </div>
</div>

Leading icon with a custom clear button

  • peer
  • peer-placeholder-shown:hidden
  • peer-focus:text-*
  • rounded-full

Type a query and press Enter. Submitting is prevented, and a result line shows what the form would send.

The clear button follows the input in the markup, so peer-placeholder-shown:hidden can hide it whenever the field is empty, with no script for the visibility. The native cancel button is switched off so only one clear control shows. A few lines of JS empty the field, return focus to it and clear on Escape; the suggestion buttons fill the field and submit it.

View code
<form role="search" class="tsr-clr w-full max-w-md rounded-3xl bg-orange-50 p-5 ring-1 ring-orange-900/10 md:p-6">
  <label for="tsr-clr-q" class="block text-lg font-bold tracking-tight text-orange-950">What are we cooking?</label>
  <div class="relative mt-3">
    <input id="tsr-clr-q" name="q" type="search" placeholder="Search 2,400 recipes" autocomplete="off" class="peer h-12 w-full rounded-full border border-orange-200 bg-white pr-12 pl-11 text-base text-stone-900 placeholder:text-stone-500 focus:border-orange-600 focus:ring-3 focus:ring-orange-600/20 focus:outline-hidden [&::-webkit-search-cancel-button]:appearance-none">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-4 size-5 -translate-y-1/2 text-stone-400 peer-focus:text-orange-700"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    <button type="button" aria-label="Clear search" class="tsr-clr-x absolute top-1/2 right-1.5 grid size-9 -translate-y-1/2 cursor-pointer place-items-center rounded-full text-stone-600 peer-placeholder-shown:hidden hover:bg-orange-100 hover:text-orange-900 focus-visible:outline-2 focus-visible:outline-orange-700">
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-4"><path d="m5 5 10 10M15 5 5 15"/></svg>
    </button>
  </div>
  <div class="mt-4 flex flex-wrap items-center gap-2 text-sm">
    <span class="text-orange-950">Popular:</span>
    <button type="button" class="tsr-clr-pick min-h-9 cursor-pointer rounded-full bg-white px-3 text-orange-900 ring-1 ring-orange-200 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">one-pot pasta</button>
    <button type="button" class="tsr-clr-pick min-h-9 cursor-pointer rounded-full bg-white px-3 text-orange-900 ring-1 ring-orange-200 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">sourdough</button>
    <button type="button" class="tsr-clr-pick min-h-9 cursor-pointer rounded-full bg-white px-3 text-orange-900 ring-1 ring-orange-200 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">miso soup</button>
  </div>
  <p role="status" class="tsr-clr-out mt-3 min-h-5 text-sm text-orange-950"></p>
</form>

Expanding search that grows on focus

  • focus-within:w-*
  • has-[input:not(:placeholder-shown)]:
  • motion-safe:transition-[width]
  • peer-focus:

Click the magnifier, or Tab to it.

The wrapper is 44px wide, the size of the icon, and focus-within: widens it when the input gets focus. has-[input:not(:placeholder-shown)]: keeps it open while it holds text, so the query never hides behind the icon. The width change animates only under motion-safe:, and the placeholder stays transparent until focus so it never peeks out of the collapsed field.

View code
<div class="tsr-exp w-full max-w-lg rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center gap-3 border-b border-slate-100 px-4 py-3">
    <h3 class="min-w-0 flex-1 truncate text-base font-semibold text-slate-900">Inbox</h3>
    <form role="search" class="relative w-11 shrink-0 focus-within:w-48 has-[input:not(:placeholder-shown)]:w-48 motion-safe:transition-[width] motion-safe:duration-300 motion-safe:ease-out sm:focus-within:w-72 sm:has-[input:not(:placeholder-shown)]:w-72">
      <input type="search" name="q" aria-label="Search messages" placeholder="Search messages" class="peer h-11 w-full cursor-pointer rounded-full border border-slate-200 bg-slate-50 pr-3 pl-11 text-base text-slate-900 placeholder:text-transparent hover:bg-slate-100 focus:cursor-text focus:border-indigo-500 focus:bg-white focus:ring-3 focus:ring-indigo-500/20 focus:outline-hidden focus:placeholder:text-slate-500 [&::-webkit-search-cancel-button]:appearance-none">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-slate-600 peer-focus:text-indigo-600"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    </form>
  </div>
  <ul class="divide-y divide-slate-100">
    <li class="flex gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-800">MK</span>
      <div class="min-w-0"><p class="truncate text-sm font-semibold text-slate-900">Mara Kowalski</p><p class="truncate text-sm text-slate-600">Updated mockups for the pricing page</p></div>
    </li>
    <li class="flex gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-amber-100 text-xs font-bold text-amber-900">TO</span>
      <div class="min-w-0"><p class="truncate text-sm font-semibold text-slate-900">Tomas Okafor</p><p class="truncate text-sm text-slate-600">Can we move the review to Thursday?</p></div>
    </li>
    <li class="flex gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-emerald-100 text-xs font-bold text-emerald-900">LB</span>
      <div class="min-w-0"><p class="truncate text-sm font-semibold text-slate-900">Lena Brandt</p><p class="truncate text-sm text-slate-600">Invoice 2041 is ready to send</p></div>
    </li>
  </ul>
  <p role="status" class="tsr-exp-out min-h-5 px-4 pb-3 text-sm text-slate-700"></p>
</div>

Start page pill with a voice button look

  • focus-within:ring-2
  • aria-pressed:
  • group-aria-pressed:
  • motion-safe:animate-ping

Click the microphone to see the listening state.

The pill draws its focus ring with focus-within:, so the input itself can drop its outline. The microphone is a toggle with aria-pressed, and aria-pressed: plus group-aria-pressed: switch its color and show a ping ring that only animates under motion-safe:. It is visual only: hook it to the Web Speech API where a browser supports it, and keep typing as the main path.

View code
<form role="search" class="tsr-voice w-full max-w-xl text-center">
  <p class="text-4xl font-bold tracking-tight text-stone-900 md:text-5xl">wayfind</p>
  <div class="mt-6 flex h-14 items-center gap-1 rounded-full bg-white pr-2 pl-5 shadow-md ring-1 ring-stone-200 focus-within:ring-2 focus-within:ring-rose-500">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-5 shrink-0 text-stone-500"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    <input type="search" name="q" aria-label="Search the web" placeholder="Search or ask" class="h-full min-w-0 flex-1 bg-transparent px-2 text-base text-stone-900 placeholder:text-stone-500 focus:outline-hidden">
    <button type="button" aria-pressed="false" aria-label="Search by voice" class="tsr-voice-mic group relative grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-rose-600 hover:bg-rose-50 focus-visible:outline-2 focus-visible:outline-rose-600 aria-pressed:bg-rose-600 aria-pressed:text-white">
      <span aria-hidden="true" class="absolute inset-0 hidden rounded-full bg-rose-500/40 group-aria-pressed:block motion-safe:animate-ping"></span>
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="relative size-5"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>
    </button>
    <button type="submit" class="grid size-10 shrink-0 cursor-pointer place-items-center rounded-full bg-stone-900 text-white hover:bg-stone-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">
      <span class="sr-only">Search</span>
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5"><path d="M4 10h12M11 5l5 5-5 5"/></svg>
    </button>
  </div>
  <p role="status" class="tsr-voice-out mt-4 min-h-10 text-sm text-stone-700"></p>
</form>

Search bars in page layouts

Where the bar sits changes how it is built: a navbar search that folds into an icon on phones, a hero search with a category select, a scoped search with a dropdown and the header of a results page with tabs.

Hero search with a category select

  • appearance-none
  • md:flex-row
  • md:border-r
  • isolate -z-10

Pick a category, then search or tap a suggestion.

The select is a native element made flat with appearance-none and given its own chevron, so it keeps keyboard and screen reader behaviour for free. The panel stacks on phones and becomes one row from md:, with md:border-r drawing the divider between select and input. A plain stone-950/60 overlay sits behind the content with -z-10 inside an isolate section, which keeps the white text readable on the photo.

View code
<section class="tsr-hero relative isolate w-full bg-stone-900 bg-cover bg-center px-4 py-14 md:py-20" style="background-image:url(canal-houses.webp)">
  <div aria-hidden="true" class="absolute inset-0 -z-10 bg-stone-950/60"></div>
  <div class="mx-auto max-w-3xl text-center">
    <h3 class="text-3xl font-bold tracking-tight text-balance text-white md:text-5xl">Slow weekends in the canal towns</h3>
    <p class="mt-3 text-base text-stone-100 md:text-lg">Small hotels, houseboats and bike routes, picked by people who live there.</p>
  </div>
  <form role="search" class="mx-auto mt-8 flex max-w-3xl flex-col gap-2 rounded-2xl bg-white p-2 shadow-xl md:flex-row md:items-center md:gap-0">
    <div class="relative md:w-44 md:shrink-0 md:border-r md:border-stone-200">
      <label for="tsr-hero-cat" class="sr-only">Category</label>
      <select id="tsr-hero-cat" class="tsr-hero-cat h-12 w-full cursor-pointer appearance-none rounded-xl bg-stone-100 pr-10 pl-4 text-base font-semibold text-stone-900 focus-visible:outline-2 focus-visible:outline-amber-600 md:bg-transparent">
        <option value="stays" data-ph="Town, area or hotel">Stays</option>
        <option value="boats" data-ph="Houseboat or canal tour">Boat trips</option>
        <option value="food" data-ph="Bakery, cafe or dinner">Food</option>
        <option value="bikes" data-ph="Bike rental or route">Bikes</option>
      </select>
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 text-stone-600"><path d="m6 8 4 4 4-4"/></svg>
    </div>
    <div class="relative min-w-0 flex-1">
      <label for="tsr-hero-q" class="sr-only">Search stays</label>
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-4 size-5 -translate-y-1/2 text-stone-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <input id="tsr-hero-q" type="search" name="q" placeholder="Town, area or hotel" class="h-12 w-full rounded-xl bg-white pr-3 pl-11 text-base text-stone-900 placeholder:text-stone-500 focus:ring-2 focus:ring-amber-500 focus:outline-hidden">
    </div>
    <button type="submit" class="h-12 shrink-0 cursor-pointer rounded-xl bg-amber-400 px-6 text-base font-semibold text-stone-950 hover:bg-amber-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600">Search</button>
  </form>
  <div class="mx-auto mt-4 flex max-w-3xl flex-wrap items-center justify-center gap-2 text-sm">
    <span class="text-white">Trending:</span>
    <button type="button" class="tsr-hero-pick min-h-9 cursor-pointer rounded-full bg-white/15 px-3 text-white ring-1 ring-white/40 hover:bg-white/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">canal view</button>
    <button type="button" class="tsr-hero-pick min-h-9 cursor-pointer rounded-full bg-white/15 px-3 text-white ring-1 ring-white/40 hover:bg-white/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">houseboat</button>
    <button type="button" class="tsr-hero-pick min-h-9 cursor-pointer rounded-full bg-white/15 px-3 text-white ring-1 ring-white/40 hover:bg-white/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">breakfast included</button>
  </div>
  <p role="status" class="tsr-hero-out mx-auto mt-4 min-h-5 max-w-3xl text-center text-sm font-semibold text-white"></p>
</section>

Search results page header with tabs

  • role="tablist"
  • aria-selected:
  • tabular-nums
  • overflow-x-auto

Click a tab, or focus one and use the arrow keys.

The query stays editable at the top of the results page, and the tabs follow the APG tabs pattern: roving tabindex, arrow keys, Home and End, aria-selected on the active tab. aria-selected: draws the underline and color, so the script never touches a class. On phones the tab row scrolls sideways inside overflow-x-auto instead of wrapping.

View code
<div class="tsr-res w-full bg-white">
  <div class="mx-auto max-w-3xl px-4 pt-6">
    <form role="search" class="flex gap-2">
      <label for="tsr-res-q" class="sr-only">Search snippets</label>
      <input id="tsr-res-q" type="search" name="q" value="dark mode toggle" class="h-11 min-w-0 flex-1 rounded-lg border border-slate-300 bg-white px-3 text-base text-slate-900 focus:border-sky-600 focus:ring-3 focus:ring-sky-600/20 focus:outline-hidden">
      <button type="submit" class="h-11 shrink-0 cursor-pointer rounded-lg bg-sky-700 px-4 text-sm font-semibold text-white hover:bg-sky-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Search</button>
    </form>
    <h3 class="mt-6 text-xl font-bold tracking-tight text-slate-900 md:text-2xl">Results for <span class="tsr-res-q">“dark mode toggle”</span></h3>
    <p class="tsr-res-meta mt-1 text-sm text-slate-600 tabular-nums">248 results</p>
    <div role="tablist" aria-label="Result types" class="tsr-res-tabs mt-4 -mx-4 flex gap-1 overflow-x-auto border-b border-slate-200 px-4 [scrollbar-width:none]">
      <button type="button" role="tab" id="tsr-res-t1" aria-selected="true" aria-controls="tsr-res-p1" data-count="248" class="-mb-px flex min-h-11 shrink-0 cursor-pointer items-center gap-2 border-b-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-700 aria-selected:border-sky-700 aria-selected:text-sky-800">All <span class="rounded-full bg-slate-100 px-2 text-xs text-slate-700 tabular-nums">248</span></button>
      <button type="button" role="tab" id="tsr-res-t2" aria-selected="false" aria-controls="tsr-res-p2" tabindex="-1" data-count="120" class="-mb-px flex min-h-11 shrink-0 cursor-pointer items-center gap-2 border-b-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-700 aria-selected:border-sky-700 aria-selected:text-sky-800">Articles <span class="rounded-full bg-slate-100 px-2 text-xs text-slate-700 tabular-nums">120</span></button>
      <button type="button" role="tab" id="tsr-res-t3" aria-selected="false" aria-controls="tsr-res-p3" tabindex="-1" data-count="86" class="-mb-px flex min-h-11 shrink-0 cursor-pointer items-center gap-2 border-b-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-700 aria-selected:border-sky-700 aria-selected:text-sky-800">Snippets <span class="rounded-full bg-slate-100 px-2 text-xs text-slate-700 tabular-nums">86</span></button>
      <button type="button" role="tab" id="tsr-res-t4" aria-selected="false" aria-controls="tsr-res-p4" tabindex="-1" data-count="42" class="-mb-px flex min-h-11 shrink-0 cursor-pointer items-center gap-2 border-b-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-700 aria-selected:border-sky-700 aria-selected:text-sky-800">Videos <span class="rounded-full bg-slate-100 px-2 text-xs text-slate-700 tabular-nums">42</span></button>
    </div>
  </div>
  <div class="mx-auto max-w-3xl px-4 pb-8">
    <div role="tabpanel" id="tsr-res-p1" aria-labelledby="tsr-res-t1" tabindex="0" class="space-y-5 pt-5 focus-visible:outline-2 focus-visible:outline-sky-700">
      <article><p class="text-xs text-slate-500">snippets › css › toggles</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Accessible dark mode toggle with a switch role</a></h4><p class="mt-1 text-sm text-slate-600">A button with aria-pressed that flips a dark class on the html element and remembers the choice.</p></article>
      <article><p class="text-xs text-slate-500">articles › theming</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Respecting prefers-color-scheme before the toggle</a></h4><p class="mt-1 text-sm text-slate-600">Start from the system setting, then let people override it on your site.</p></article>
    </div>
    <div role="tabpanel" id="tsr-res-p2" aria-labelledby="tsr-res-t2" tabindex="0" hidden class="space-y-5 pt-5 focus-visible:outline-2 focus-visible:outline-sky-700">
      <article><p class="text-xs text-slate-500">articles › theming</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Respecting prefers-color-scheme before the toggle</a></h4><p class="mt-1 text-sm text-slate-600">Start from the system setting, then let people override it on your site.</p></article>
      <article><p class="text-xs text-slate-500">articles › color</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Picking dark palette colors that still pass contrast</a></h4><p class="mt-1 text-sm text-slate-600">Why pure black backgrounds tire the eyes, and what to use instead.</p></article>
    </div>
    <div role="tabpanel" id="tsr-res-p3" aria-labelledby="tsr-res-t3" tabindex="0" hidden class="space-y-5 pt-5 focus-visible:outline-2 focus-visible:outline-sky-700">
      <article><p class="text-xs text-slate-500">snippets › css › toggles</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Accessible dark mode toggle with a switch role</a></h4><p class="mt-1 text-sm text-slate-600">A button with aria-pressed that flips a dark class on the html element and remembers the choice.</p></article>
      <article><p class="text-xs text-slate-500">snippets › tailwind › theme</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Sun and moon icon swap with group-aria-pressed</a></h4><p class="mt-1 text-sm text-slate-600">Two inline icons, one button, zero class toggling in the script.</p></article>
    </div>
    <div role="tabpanel" id="tsr-res-p4" aria-labelledby="tsr-res-t4" tabindex="0" hidden class="space-y-5 pt-5 focus-visible:outline-2 focus-visible:outline-sky-700">
      <article><p class="text-xs text-slate-500">videos · 6:12</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Build a dark mode toggle in ten minutes</a></h4><p class="mt-1 text-sm text-slate-600">From a plain button to a themed site, with the flash of light theme fixed.</p></article>
      <article><p class="text-xs text-slate-500">videos · 3:40</p><h4 class="mt-0.5 text-base font-semibold"><a href="#" class="text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Animating the theme switch without motion sickness</a></h4><p class="mt-1 text-sm text-slate-600">Short cross-fades that respect prefers-reduced-motion.</p></article>
    </div>
    <p role="status" class="tsr-res-out mt-5 min-h-5 text-sm text-slate-700"></p>
  </div>
</div>

Autocomplete, command palette and recent searches

Search bars that answer while you type. The combobox and the command palette follow the W3C APG combobox pattern: focus stays in the input, aria-activedescendant points at the highlighted option, and the arrow keys, Enter and Escape do what users expect.

Autocomplete combobox with highlighted matches

  • role="combobox"
  • aria-activedescendant
  • aria-selected:
  • <mark>

Type bor or sh, then use the arrow keys and Enter. Escape closes the list, a second Escape clears the field.

This is the APG combobox with list autocomplete: focus stays in the input, aria-expanded reports the list, and aria-activedescendant names the highlighted option while the arrow keys move it. aria-selected: and group-aria-selected: paint the active row, so the script only flips attributes. Matching text is wrapped in a mark element built with createElement and text nodes, never with innerHTML.

View code
<div class="tsr-ac w-full max-w-md self-start rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-900/5 md:p-6">
  <form role="search">
    <label for="tsr-ac-q" class="block text-sm font-semibold text-zinc-900">Jump to a docs page</label>
    <div class="relative mt-2">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-zinc-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <input id="tsr-ac-q" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="tsr-ac-list" aria-describedby="tsr-ac-status" autocomplete="off" spellcheck="false" placeholder="Border radius, shadow, grid" class="tsr-ac-input h-11 w-full rounded-lg border border-zinc-300 bg-white pr-3 pl-10 text-base text-zinc-900 placeholder:text-zinc-500 focus:border-emerald-700 focus:ring-3 focus:ring-emerald-700/20 focus:outline-hidden">
      <ul id="tsr-ac-list" role="listbox" aria-label="Docs pages" hidden class="absolute inset-x-0 top-full z-10 mt-1.5 max-h-64 overflow-y-auto rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-zinc-900/10"></ul>
    </div>
    <p id="tsr-ac-status" role="status" class="tsr-ac-status mt-2 min-h-5 text-sm text-zinc-600"></p>
  </form>
  <p class="tsr-ac-out min-h-5 text-sm font-medium text-emerald-800"></p>
</div>

Command palette in a dialog with Ctrl or Cmd+K

  • <dialog>
  • backdrop:
  • starting:open:
  • <kbd>

Press Ctrl+K (Cmd+K on a Mac) or click the bar. Arrow keys move, Enter runs, Escape closes.

The palette is a native dialog opened with showModal(), so the page behind it goes inert, Escape closes it and focus returns to the trigger. backdrop: tints the page and starting:open: fades the panel in under motion-safe:. Inside, the input is a combobox over a grouped listbox, and the kbd hints switch between Ctrl and the Command symbol depending on the platform.

View code
<div class="tsr-cmd w-full max-w-md self-start">
  <div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-900/5 md:p-6">
    <p class="text-base font-semibold text-zinc-900">Ledgerline</p>
    <p class="mt-1 text-sm text-zinc-600">Jump to any page or run an action without leaving the keyboard.</p>
    <button type="button" aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K" class="tsr-cmd-open mt-4 flex h-11 w-full cursor-pointer items-center gap-3 rounded-lg bg-zinc-100 px-3 text-left text-sm text-zinc-600 ring-1 ring-zinc-200 hover:bg-zinc-200/70 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-4 shrink-0"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <span class="flex-1">Search or jump to</span>
      <span aria-hidden="true" class="flex gap-1"><kbd class="tsr-cmd-mod rounded border border-zinc-300 bg-white px-1.5 font-sans text-xs font-semibold text-zinc-600">Ctrl</kbd><kbd class="rounded border border-zinc-300 bg-white px-1.5 font-sans text-xs font-semibold text-zinc-600">K</kbd></span>
    </button>
    <p role="status" class="tsr-cmd-out mt-3 min-h-5 text-sm text-zinc-700"></p>
  </div>
  <dialog id="tsr-cmd-dlg" aria-label="Command palette" class="mx-auto mt-12 w-[calc(100%-2rem)] max-w-lg overflow-hidden rounded-2xl bg-white p-0 shadow-2xl ring-1 ring-zinc-900/10 backdrop:bg-zinc-950/40 motion-safe:transition-[opacity,scale] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:scale-95 motion-safe:starting:open:opacity-0">
    <div class="flex items-center gap-3 border-b border-zinc-200 px-4">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-5 shrink-0 text-zinc-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <input id="tsr-cmd-q" type="text" role="combobox" aria-expanded="true" aria-controls="tsr-cmd-list" aria-autocomplete="list" aria-label="Search pages and actions" autocomplete="off" spellcheck="false" placeholder="Type a page or an action" class="h-14 w-full min-w-0 bg-transparent text-base text-zinc-900 placeholder:text-zinc-500 focus:outline-hidden">
      <kbd class="hidden shrink-0 rounded border border-zinc-300 px-1.5 font-sans text-xs font-semibold text-zinc-600 sm:block">Esc</kbd>
      <button type="button" class="tsr-cmd-close shrink-0 cursor-pointer rounded-md px-2 py-1 text-sm font-semibold text-blue-700 hover:bg-blue-50 focus-visible:outline-2 focus-visible:outline-blue-600 sm:hidden">Close</button>
    </div>
    <div id="tsr-cmd-list" role="listbox" aria-label="Pages and actions" class="max-h-72 overflow-y-auto p-2">
      <div role="group" aria-labelledby="tsr-cmd-g1" class="tsr-cmd-group">
        <div id="tsr-cmd-g1" role="presentation" class="px-3 pt-2 pb-1 text-xs font-semibold text-zinc-500">Pages</div>
        <div role="option" id="tsr-cmd-o1" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5 text-zinc-400 group-aria-selected:text-white"><path d="M3 9.5 10 4l7 5.5V16H3z"/></svg><span class="tsr-cmd-label flex-1">Dashboard</span></div>
        <div role="option" id="tsr-cmd-o2" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5 text-zinc-400 group-aria-selected:text-white"><path d="M5 3h10v14l-2.5-1.5L10 17l-2.5-1.5L5 17zM8 7h4M8 10h4"/></svg><span class="tsr-cmd-label flex-1">Invoices</span></div>
        <div role="option" id="tsr-cmd-o3" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5 text-zinc-400 group-aria-selected:text-white"><circle cx="10" cy="7" r="3"/><path d="M4 17c1-3 3.5-4.5 6-4.5s5 1.5 6 4.5"/></svg><span class="tsr-cmd-label flex-1">Customers</span></div>
        <div role="option" id="tsr-cmd-o4" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5 text-zinc-400 group-aria-selected:text-white"><rect x="3" y="5" width="14" height="10" rx="2"/><path d="M3 8.5h14"/></svg><span class="tsr-cmd-label flex-1">Billing settings</span></div>
      </div>
      <div role="group" aria-labelledby="tsr-cmd-g2" class="tsr-cmd-group">
        <div id="tsr-cmd-g2" role="presentation" class="px-3 pt-3 pb-1 text-xs font-semibold text-zinc-500">Actions</div>
        <div role="option" id="tsr-cmd-o5" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" class="size-5 text-zinc-400 group-aria-selected:text-white"><path d="M10 4v12M4 10h12"/></svg><span class="tsr-cmd-label flex-1">Create invoice</span><kbd class="rounded border border-zinc-300 px-1.5 font-sans text-xs text-zinc-600 group-aria-selected:border-white/50 group-aria-selected:text-white">N</kbd></div>
        <div role="option" id="tsr-cmd-o6" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" class="size-5 text-zinc-400 group-aria-selected:text-white"><circle cx="8" cy="7" r="3"/><path d="M2.5 17c.8-3 3-4.5 5.5-4.5M15 11v6M12 14h6"/></svg><span class="tsr-cmd-label flex-1">Invite a teammate</span></div>
        <div role="option" id="tsr-cmd-o7" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" class="size-5 text-zinc-400 group-aria-selected:text-white"><path d="M10 3v10M6 9l4 4 4-4M4 16h12"/></svg><span class="tsr-cmd-label flex-1">Export customers as CSV</span></div>
        <div role="option" id="tsr-cmd-o8" aria-selected="false" class="tsr-cmd-opt group flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-800 aria-selected:bg-blue-600 aria-selected:text-white"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5 text-zinc-400 group-aria-selected:text-white"><path d="M15.5 12.5A6 6 0 0 1 7.5 4.5a6 6 0 1 0 8 8z"/></svg><span class="tsr-cmd-label flex-1">Switch to dark theme</span></div>
      </div>
      <p class="tsr-cmd-empty px-3 py-8 text-center text-sm text-zinc-600" hidden>No pages or actions match.</p>
    </div>
    <div class="hidden items-center gap-4 border-t border-zinc-200 bg-zinc-50 px-4 py-2.5 text-xs text-zinc-600 sm:flex">
      <span class="flex items-center gap-1"><kbd class="rounded border border-zinc-300 bg-white px-1 font-sans font-semibold">↑</kbd><kbd class="rounded border border-zinc-300 bg-white px-1 font-sans font-semibold">↓</kbd> to move</span>
      <span class="flex items-center gap-1"><kbd class="rounded border border-zinc-300 bg-white px-1 font-sans font-semibold">↵</kbd> to run</span>
      <span class="flex items-center gap-1"><kbd class="rounded border border-zinc-300 bg-white px-1 font-sans font-semibold">Esc</kbd> to close</span>
    </div>
  </dialog>
</div>

Recent searches kept in memory

  • group-focus-within:block
  • group-has-[input:not(:placeholder-shown)]:hidden
  • group-data-dismissed:
  • <template>

Click into the field to see recent searches. Search for something to add it.

Three variants decide when the panel shows, with no script: group-focus-within:block opens it while the form has focus, group-has-[input:not(:placeholder-shown)]:hidden hides it once the user types, and group-data-dismissed:hidden lets Escape close it. Rows are cloned from a template element, so the classes stay in HTML where Tailwind can see them. The list lives in a JavaScript array only and is gone after a reload, which is the privacy-friendly default for a shared device.

View code
<form role="search" class="tsr-rec group relative w-full max-w-md self-start rounded-2xl bg-white p-5 shadow-sm ring-1 ring-stone-900/5 md:p-6">
  <label for="tsr-rec-q" class="block text-sm font-semibold text-stone-900">Search the outdoor shop</label>
  <div class="relative mt-2">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3.5 size-5 -translate-y-1/2 text-stone-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    <input id="tsr-rec-q" type="search" name="q" placeholder="Jackets, tents, stoves" autocomplete="off" aria-describedby="tsr-rec-hint" class="h-12 w-full rounded-xl border border-stone-300 bg-white pr-4 pl-11 text-base text-stone-900 placeholder:text-stone-500 focus:border-rose-700 focus:ring-3 focus:ring-rose-700/20 focus:outline-hidden">
    <div class="tsr-rec-panel absolute inset-x-0 top-full z-10 mt-2 hidden rounded-xl bg-white p-2 shadow-lg ring-1 ring-stone-900/10 group-focus-within:block group-has-[input:not(:placeholder-shown)]:hidden group-data-dismissed:hidden">
      <div class="flex items-center justify-between px-2 py-1">
        <p id="tsr-rec-title" class="text-sm font-semibold text-stone-700">Recent searches</p>
        <button type="button" class="tsr-rec-clear cursor-pointer rounded-md px-2 py-1 text-sm font-semibold text-rose-800 hover:bg-rose-50 focus-visible:outline-2 focus-visible:outline-rose-700">Clear all</button>
      </div>
      <ul class="tsr-rec-list" aria-labelledby="tsr-rec-title"></ul>
      <p class="tsr-rec-none px-2 py-3 text-sm text-stone-600" hidden>No recent searches yet.</p>
    </div>
  </div>
  <p id="tsr-rec-hint" class="sr-only">While the field is empty, recent searches appear below it. Press Tab to reach them.</p>
  <p role="status" class="tsr-rec-out mt-3 min-h-10 text-sm text-stone-700"></p>
  <template class="tsr-rec-tpl">
    <li class="flex items-center gap-1">
      <button type="button" class="tsr-rec-run flex min-h-11 min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-lg px-2 text-left text-sm text-stone-800 hover:bg-stone-100 focus-visible:outline-2 focus-visible:outline-rose-700">
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" class="size-5 shrink-0 text-stone-400"><circle cx="10" cy="10" r="7"/><path d="M10 6v4l2.5 2"/></svg>
        <span class="tsr-rec-term truncate"></span>
      </button>
      <button type="button" class="tsr-rec-del grid size-11 shrink-0 cursor-pointer place-items-center rounded-lg text-stone-500 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-rose-700">
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-4"><path d="m5 5 10 10M15 5 5 15"/></svg>
      </button>
    </li>
  </template>
</form>

Debounced live search with a loading state

  • group-data-loading:
  • motion-safe:animate-spin
  • aria-busy
  • tabular-nums

Type card or menu quickly and watch the request counter.

The input waits 300 ms after the last keystroke before it sends a request, and each request carries a sequence number so a slow, older answer can never overwrite a newer one (with fetch, pass an AbortController signal instead). A data-loading attribute on the root drives the spinner through group-data-loading:, and the spinner only rotates under motion-safe:. The counters show how many keystrokes collapsed into how many requests.

View code
<div class="tsr-deb group w-full max-w-md rounded-2xl bg-slate-900 p-5 ring-1 ring-white/10 md:p-6">
  <label for="tsr-deb-q" class="block text-sm font-semibold text-slate-100">Search the component library</label>
  <div class="relative mt-2">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-slate-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    <input id="tsr-deb-q" type="search" autocomplete="off" placeholder="Dialog, tabs, toast" aria-controls="tsr-deb-list" class="h-11 w-full rounded-lg border border-slate-700 bg-slate-800 pr-11 pl-10 text-base text-slate-100 placeholder:text-slate-400 focus:border-sky-400 focus:ring-3 focus:ring-sky-400/25 focus:outline-hidden [&::-webkit-search-cancel-button]:appearance-none">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" class="pointer-events-none absolute top-1/2 right-3 hidden size-5 -translate-y-1/2 text-sky-300 group-data-loading:block motion-safe:animate-spin"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".25" stroke-width="3"/><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
  </div>
  <p class="mt-2 flex gap-4 text-xs text-slate-400 tabular-nums"><span>Keystrokes <span class="tsr-deb-keys font-semibold text-slate-200">0</span></span><span>Requests <span class="tsr-deb-reqs font-semibold text-slate-200">0</span></span></p>
  <p role="status" class="tsr-deb-status mt-3 min-h-5 text-sm text-slate-300">Start typing to search 24 components.</p>
  <ul id="tsr-deb-list" aria-label="Results" aria-busy="false" class="tsr-deb-list mt-2 divide-y divide-white/5 group-data-loading:opacity-60"></ul>
</div>

Live filters, chips and dark mode

Search fields that filter what is already on the page: a list with a polite result count and an empty state, a job board with checkbox chips styled through has-checked:, and a docs search that switches between light and dark themes.

Live filter with a polite count and an empty state

  • role="status"
  • sr-only
  • md:grid-cols-2
  • hidden

Type pay, mail or xyz.

The field filters cards that are already on the page, so there is no request and no debounce for the visible list. The visible counter updates on every keystroke, while a separate sr-only live region waits half a second before it speaks, so a screen reader hears one result count instead of one per letter. When nothing matches, an empty state explains why and offers a one-click reset.

View code
<div class="tsr-lf w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-neutral-900/5 md:p-6">
  <div class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
    <div>
      <h3 class="text-lg font-bold tracking-tight text-neutral-900">Integrations</h3>
      <p class="tsr-lf-count text-sm text-neutral-600 tabular-nums">12 integrations</p>
    </div>
    <div class="relative md:w-64">
      <label for="tsr-lf-q" class="sr-only">Filter integrations</label>
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-neutral-500"><path d="M3 5h14M6 10h8M9 15h2"/></svg>
      <input id="tsr-lf-q" type="search" placeholder="Filter by name or category" autocomplete="off" aria-controls="tsr-lf-list" class="h-11 w-full rounded-lg border border-neutral-300 bg-neutral-50 pr-3 pl-9 text-base text-neutral-900 placeholder:text-neutral-500 focus:border-lime-700 focus:bg-white focus:ring-3 focus:ring-lime-700/20 focus:outline-hidden md:text-sm">
    </div>
  </div>
  <p role="status" class="tsr-lf-sr sr-only"></p>
  <ul id="tsr-lf-list" class="mt-5 grid gap-2 md:grid-cols-2">
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-lime-200 text-sm font-bold text-lime-950">BC</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Bramble CRM</span><span class="block text-sm text-neutral-600">Sales</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-sky-200 text-sm font-bold text-sky-950">FM</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Ferry Mail</span><span class="block text-sm text-neutral-600">Email</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-orange-200 text-sm font-bold text-orange-950">KA</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Kiln Analytics</span><span class="block text-sm text-neutral-600">Analytics</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-emerald-200 text-sm font-bold text-emerald-950">LP</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Lumen Pay</span><span class="block text-sm text-neutral-600">Payments</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-violet-200 text-sm font-bold text-violet-950">OC</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Orbit Chat</span><span class="block text-sm text-neutral-600">Support</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-amber-200 text-sm font-bold text-amber-950">PS</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Parcel Ship</span><span class="block text-sm text-neutral-600">Shipping</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-rose-200 text-sm font-bold text-rose-950">QD</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Quill Docs</span><span class="block text-sm text-neutral-600">Documents</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-teal-200 text-sm font-bold text-teal-950">RF</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Relay Forms</span><span class="block text-sm text-neutral-600">Email</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-indigo-200 text-sm font-bold text-indigo-950">SC</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Sable Calendar</span><span class="block text-sm text-neutral-600">Scheduling</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-cyan-200 text-sm font-bold text-cyan-950">TS</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Tidewater Storage</span><span class="block text-sm text-neutral-600">Files</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-stone-300 text-sm font-bold text-stone-950">UA</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Umbra Auth</span><span class="block text-sm text-neutral-600">Security</span></span></li>
    <li class="tsr-lf-item flex items-center gap-3 rounded-xl p-3 ring-1 ring-neutral-200"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-fuchsia-200 text-sm font-bold text-fuchsia-950">WP</span><span class="min-w-0"><span class="block truncate text-sm font-semibold text-neutral-900">Wren Payroll</span><span class="block text-sm text-neutral-600">Payments</span></span></li>
  </ul>
  <div class="tsr-lf-empty mt-5 rounded-xl border border-dashed border-neutral-300 px-4 py-8 text-center" hidden>
    <p class="text-sm font-semibold text-neutral-900">No integrations match <span class="tsr-lf-term"></span></p>
    <p class="mt-1 text-sm text-neutral-600">Check the spelling, or search by category such as email or payments.</p>
    <button type="button" class="tsr-lf-reset mt-4 min-h-10 cursor-pointer rounded-lg bg-neutral-900 px-4 text-sm font-semibold text-white hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900">Clear filter</button>
  </div>
</div>

Search with checkbox filter chips

  • has-checked:
  • has-focus-visible:
  • group-has-checked:
  • type="reset"

Toggle Remote and Design, or type a role.

Each chip is a label around a visually hidden checkbox, so it is keyboard and screen reader friendly by default, and has-checked: fills it while has-focus-visible: draws its focus ring. The Clear button is a native reset button that only appears through group-has-checked: on the form once a chip is on. The script combines the text query with every checked chip and reports the count in a status line.

View code
<form role="search" class="tsr-chip group w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-900/5 md:p-6">
  <label for="tsr-chip-q" class="block text-lg font-bold tracking-tight text-zinc-900">Find your next role</label>
  <div class="relative mt-3">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-zinc-400"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
    <input id="tsr-chip-q" type="search" name="q" placeholder="Job title or company" autocomplete="off" class="h-11 w-full rounded-lg border border-zinc-300 bg-white pr-3 pl-10 text-base text-zinc-900 placeholder:text-zinc-500 focus:border-zinc-900 focus:ring-3 focus:ring-zinc-900/15 focus:outline-hidden">
  </div>
  <fieldset class="mt-4">
    <legend class="sr-only">Filters</legend>
    <div class="flex flex-wrap items-center gap-2">
      <label class="flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 select-none hover:bg-zinc-50 has-checked:bg-zinc-900 has-checked:text-white has-checked:ring-zinc-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="checkbox" name="tag" value="remote" class="peer sr-only"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" class="hidden size-3.5 peer-checked:block"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Remote</label>
      <label class="flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 select-none hover:bg-zinc-50 has-checked:bg-zinc-900 has-checked:text-white has-checked:ring-zinc-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="checkbox" name="tag" value="full-time" class="peer sr-only"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" class="hidden size-3.5 peer-checked:block"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Full-time</label>
      <label class="flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 select-none hover:bg-zinc-50 has-checked:bg-zinc-900 has-checked:text-white has-checked:ring-zinc-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="checkbox" name="tag" value="contract" class="peer sr-only"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" class="hidden size-3.5 peer-checked:block"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Contract</label>
      <label class="flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 select-none hover:bg-zinc-50 has-checked:bg-zinc-900 has-checked:text-white has-checked:ring-zinc-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="checkbox" name="tag" value="design" class="peer sr-only"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" class="hidden size-3.5 peer-checked:block"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Design</label>
      <label class="flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 select-none hover:bg-zinc-50 has-checked:bg-zinc-900 has-checked:text-white has-checked:ring-zinc-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="checkbox" name="tag" value="engineering" class="peer sr-only"><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" class="hidden size-3.5 peer-checked:block"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Engineering</label>
      <button type="reset" class="hidden min-h-9 cursor-pointer rounded-full px-3 text-sm font-semibold text-zinc-700 underline underline-offset-2 group-has-checked:inline-flex group-has-checked:items-center hover:text-zinc-950 focus-visible:outline-2 focus-visible:outline-zinc-900">Clear</button>
    </div>
  </fieldset>
  <p role="status" class="tsr-chip-out mt-4 text-sm font-medium text-zinc-700 tabular-nums">8 open roles</p>
  <ul class="mt-2 divide-y divide-zinc-100">
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="remote full-time design"><span class="text-sm font-semibold text-zinc-900">Senior product designer</span><span class="text-sm text-zinc-600">Fernhill · Remote</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="full-time engineering"><span class="text-sm font-semibold text-zinc-900">Frontend engineer</span><span class="text-sm text-zinc-600">Ledgerline · Lisbon</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="remote contract design"><span class="text-sm font-semibold text-zinc-900">Brand illustrator</span><span class="text-sm text-zinc-600">Kettle and Co · Remote</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="remote full-time engineering"><span class="text-sm font-semibold text-zinc-900">Platform engineer</span><span class="text-sm text-zinc-600">Quarry · Remote</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="contract engineering"><span class="text-sm font-semibold text-zinc-900">WordPress developer</span><span class="text-sm text-zinc-600">Oakline Studio · Leeds</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="full-time design"><span class="text-sm font-semibold text-zinc-900">UX researcher</span><span class="text-sm text-zinc-600">Wayfind · Berlin</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="remote contract design"><span class="text-sm font-semibold text-zinc-900">Webflow designer</span><span class="text-sm text-zinc-600">Northgate · Remote</span></li>
    <li class="tsr-chip-job flex flex-wrap items-baseline justify-between gap-x-3 py-3" data-tags="full-time engineering"><span class="text-sm font-semibold text-zinc-900">Design systems engineer</span><span class="text-sm text-zinc-600">Acorn · Toronto</span></li>
  </ul>
  <p class="tsr-chip-none py-6 text-center text-sm text-zinc-600" hidden>No roles match these filters. Remove a chip to see more.</p>
</form>

Docs search with a light and dark theme

  • dark:
  • dark:scheme-dark
  • dark:placeholder:
  • group-aria-pressed:

Toggle the theme, then type grid or color.

Every color on the bar has a dark: partner: background, border, text, placeholder, icon, ring and the result rows. The toggle adds or removes the dark class on the wrapper, which the @custom-variant line turns into a dark scope, and dark:scheme-dark tells the browser to draw native parts, such as the cancel button, in their dark style. The field also filters the six results below it.

View code
<div class="tsr-dk dark w-full max-w-md">
  <div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-900 dark:ring-white/10 dark:scheme-dark md:p-6">
    <div class="flex items-center justify-between gap-3">
      <p class="text-base font-semibold text-slate-900 dark:text-white">Stylebook docs</p>
      <button type="button" aria-pressed="true" class="tsr-dk-toggle group flex min-h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium text-slate-700 ring-1 ring-slate-200 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:text-slate-200 dark:ring-slate-700 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400">
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 group-aria-pressed:hidden"><circle cx="10" cy="10" r="3.5"/><path d="M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4"/></svg>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="hidden size-4 group-aria-pressed:block"><path d="M15.5 12.5A6 6 0 0 1 7.5 4.5a6 6 0 1 0 8 8z"/></svg>
        Dark theme
      </button>
    </div>
    <form role="search" class="relative mt-4">
      <label for="tsr-dk-q" class="sr-only">Search docs</label>
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="pointer-events-none absolute top-1/2 left-3 size-5 -translate-y-1/2 text-slate-400 dark:text-slate-500"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <input id="tsr-dk-q" type="search" name="q" placeholder="Search the docs" autocomplete="off" class="h-11 w-full rounded-lg border border-slate-200 bg-slate-100 pr-3 pl-10 text-base text-slate-900 placeholder:text-slate-500 focus:border-indigo-500 focus:bg-white focus:ring-3 focus:ring-indigo-500/25 focus:outline-hidden dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-400 dark:focus:border-indigo-400 dark:focus:bg-slate-800 dark:focus:ring-indigo-400/30">
    </form>
    <ul class="mt-4 space-y-1">
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Layout</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Grid and columns</span></a></li>
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Tokens</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Color palette</span></a></li>
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Tokens</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Dark mode color pairs</span></a></li>
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Components</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Search field</span></a></li>
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Components</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Data grid</span></a></li>
      <li class="tsr-dk-row"><a href="#" class="block rounded-lg px-3 py-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:hover:bg-slate-800 dark:focus-visible:outline-indigo-400"><span class="block text-xs text-slate-500 dark:text-slate-400">Guides</span><span class="block text-sm font-semibold text-slate-900 dark:text-slate-100">Typography scale</span></a></li>
    </ul>
    <p role="status" class="tsr-dk-out mt-3 min-h-5 px-3 text-sm text-slate-600 dark:text-slate-300"></p>
  </div>
</div>

How to Build a Tailwind Search Bar

A Tailwind search bar is a form, a search input and a submit button, styled with utility classes instead of a prebuilt stylesheet. Every visual choice (border, radius, padding, focus ring, width) sits in the class attribute of the element it changes.

No shared search class exists to override later. That’s pleasant until you need to change the same thing in six places.

Write the markup in plain HTML first, then add classes. Utility classes cannot repair a missing form wrapper or a missing name attribute.

MDN marks the search input type as widely available across browsers since July 2015. It behaves like a text input, though some browsers add a built-in clear icon and Chrome clears the field on Escape.

  1. Wrap the parts in a form with an action URL and role=”search”, so Enter submits the query.
  2. Add a label tied to the input through for and id. Hide it with sr-only if the design shows no visible label.
  3. Add input type=”search” with an id and name=”q”. Without a name, nothing is submitted.
  4. Style the input: w-full, rounded-md, border, border-gray-300, px-4, py-2.
  5. Add a submit button with a background color, padding and a hover state.

The input classes below follow the same pattern as styling any Tailwind input, with the border color set explicitly.

<form role="search" action="/search" class="flex w-full max-w-md gap-2">
  <label for="site-search" class="sr-only">Search</label>
  <input type="search" id="site-search" name="q" placeholder="Search the site"
    class="w-full rounded-md border border-gray-300 px-4 py-2 focus:outline-hidden focus:ring-3 focus:ring-blue-500">
  <button type="submit"
      class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">Search</button>
  </form>

This breaks in two usual ways. An input with no form around it does nothing on Enter, and a form whose input has no name submits nothing.

How to Add a Search Icon to the Input

Wrap the input in a relative container, place the icon with absolute positioning, and add left padding to the input so typed text clears the icon.

<div class="relative w-full max-w-md">
  <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
    <svg class="size-5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor"
        stroke-width="2" stroke-linecap="round" aria-hidden="true">
      <circle cx="11" cy="11" r="7"/>
      <path d="m20 20-4-4"/>
    </svg>
  </div>
  <input type="search" name="q" aria-label="Search"
    class="w-full rounded-md border border-gray-300 py-2 pr-4 pl-10">
</div>

The icon wrapper uses pl-3 (12px) plus a 20px size-5 icon, which fills 32px. The input’s pl-10 (40px) leaves an 8px gap before the text.

Use an inline SVG rather than an image file, because the icon then inherits currentColor and follows any text utility. pointer-events-none on the wrapper lets clicks fall through to the input.

Heroicons, made by the Tailwind team under an MIT license, includes a magnifying-glass icon. Lucide is a common alternative.

Left or right icon placement

Left is what most users expect, and it takes absolute left-0 on the wrapper with pl-10 on the input. A right-hand icon uses absolute right-0 and pr-10 instead, and it fits better when the icon doubles as the submit button.

Text running under the icon is the usual bug. It happens when the input padding is smaller than the icon wrapper.

Search button inside or outside the input

With the button outside, the layout is a flex row with gap-2 and the input set to flex-1. Input and button keep separate focus rings, and disabled styling stays simple.

Inside is fiddlier. The button sits absolute at right-1 and inset-y-1 over the input, and the input needs generous right padding, such as pr-24, so text never runs under it. What you get back is horizontal space on narrow screens.

Material Tailwind’s documentation shows both layouts, plus dropdown and Algolia variants. Outside is the safer default. Inside looks tighter.

Clear button

A clear button should appear only when the field has text. Its click handler empties the value and returns focus to the input.

  • Add peer to the input and peer-placeholder-shown:hidden to the button
  • Keep placeholder text on the input, because the placeholder-shown state never matches without it
  • Hide the native icon with [&::-webkit-search-cancel-button]:appearance-none

MDN documents a built-in clear icon in some browsers for search fields, so the native control and a custom one can both show at once.

Focus, Hover and Disabled States for the Input

The input ring comes from focus:. The icon tint comes from focus-within: on the wrapper, the button darkens with hover:, and disabled: dims both.

State variants for the input and button

On the input, focus: sets the ring and border color. Browsers treat every focus on a text input as keyboard-visible, so focus-visible: gives the same result there and earns its place on the button.

Put focus-within: on the wrapper and the icon color and border change together when the input is active. hover: is a background shift on the button, and I’d keep it purely cosmetic. While a query runs, disabled: with opacity-50 and cursor-not-allowed covers both parts.

Buttons render with cursor: default in v4, so the search button needs an explicit cursor-pointer.

Tailwind v4 defaults that change a search input

Tailwind v4.0 shipped in January 2025 with new defaults for the ring, border, outline and placeholder color, and each one alters a search input without a single edit to the markup.

DefaultTailwind v3Tailwind v4
Bare ring3px wide, blue-5001px wide, currentColor
Border colorgray-200currentColor
outline-noneTransparent outline, kept in forced colors modeRenamed outline-hidden; new outline-none removes the outline
Placeholder colorgray-400Current text color at 50% opacity

These values come from the Tailwind CSS upgrade guide, as of the v4.3 documentation. The guide recommends replacing bare ring with ring-3 and adding ring-blue-500 wherever the old blue default mattered.

Why the focus ring looks weaker after an upgrade

The bare ring shrank from 3px to 1px and lost its blue default, so a search input migrated by find and replace ends up with a thin ring in the text color.

focus:outline-hidden focus:ring-3 focus:ring-blue-500
  • Use outline-hidden, not outline-none, to remove the browser outline. The hidden variant keeps an outline in forced colors mode.
  • Set border-gray-300 explicitly, because a bare border now follows the text color.
  • If the 50% placeholder looks too dark, placeholder:text-gray-400 brings the old shade back.

A pale ring on a white page disappears, so pick a shade with enough contrast between the ring and the background.

Dark Mode for a Tailwind Search Bar

Every light-mode color on the bar needs a dark: counterpart. That covers the input background, border, text, placeholder and icon.

By default the dark variant follows the prefers-color-scheme media feature, so the bar switches with the operating system and needs no setup, per the Tailwind CSS documentation.

bg-white text-gray-900 border-gray-300 placeholder:text-gray-400
  dark:bg-gray-800 dark:text-gray-100 dark:border-gray-600 dark:placeholder:text-gray-500

For a manual toggle, override the variant in your CSS so it keys off a class on the html element.

@import "tailwindcss";
  @custom-variant dark (&:where(.dark, .dark *));

Now dark: utilities apply whenever a dark class sits higher in the tree. A data-theme attribute set to dark works the same way with a different selector.

A bare border draws in the text color under v4, so in dark mode it turns into a light line. Set the color for both modes.

Icon and clear button colors need dark variants too, such as dark:text-gray-400, and the focus ring from the previous section wants a lighter shade on dark backgrounds, for example dark:focus:ring-blue-400.

Add the scheme-dark utility on the html element in dark mode, so native form controls render in the dark palette.

Responsive Layout, Navbar Placement and Expandable Search

Keep the bar full width by default, then cap it with a breakpoint prefix such as md:max-w-sm. Unprefixed classes handle phones, and each prefix only adds changes from that width up.

Tailwind’s documentation lists sm at 40rem (640px), md at 48rem (768px) and lg at 64rem (1024px) as default breakpoints.

The system follows a mobile-first design approach, so sm: means “at the small breakpoint”, not “on small screens”. Styling the phone layout means writing the unprefixed class.

Search bar in a navbar

  • Under md, full width below the nav row, or collapsed behind an icon
  • From md, inline in the row with md:max-w-sm
  • From lg, a wider cap with lg:max-w-md

Give the form flex-1 so it shrinks before the nav links wrap. The same wrapper fits a navbar built with Tailwind that carries a logo, links and a profile menu.

Expandable search bar

In the collapsed state the input shrinks to the size of its icon, then widens on focus.

<input type="search" name="q" aria-label="Search"
  class="w-10 cursor-pointer rounded-full border border-gray-300 py-2 pl-10 pr-0 transition-[width] duration-200 focus:w-56 focus:cursor-text focus:pr-4">

With the icon positioned inside a relative wrapper, only the icon shows until the field gets focus. The aria-label stays because no visible text remains.

Don’t drive the expansion with hover. It breaks on touch screens, and Tailwind v4 applies the hover variant only when the primary input device supports hover, per the upgrade guide. Use focus: or a click handler.

Making the Search Bar Accessible

Give the input a label, mark the form as a search landmark, and add ARIA only where native HTML falls short.

WebAIM’s 2026 analysis of one million home pages found missing form input labels on 51% of pages, and a third (33.1%) of all form inputs were not properly labeled.

The best label is a visible one, a label element tied to the input through for and id, since clicking it focuses the field. If the layout shows only a magnifier icon and placeholder, an sr-only label keeps the same association. aria-label works as a non-visual stand-in when the markup has no room for a label at all.

MDN advises against leaning on the placeholder attribute, because it carries less meaning than a real label and causes unexpected technical problems. Treat it as a hint, never as the name.

For the landmark, wrap the bar in a search element, or put role=”search” on the form. MDN presents them as alternatives, since the element uses native semantics while the role has wider support. The search element itself has been Baseline widely available since April 2026 (Chrome and Firefox 118, Safari 17).

<search>
  <form action="/search">
    <label for="site-search" class="sr-only">Search the site</label>
    <input type="search" id="site-search" name="q" class="w-full rounded-md border border-gray-300 px-4 py-2">
  </form>
</search>

Landmark adoption is still thin. Only 18.8% of home pages had a search landmark in 2026, up from 16.6% a year earlier (WebAIM).

Labels are one rule among several that apply to every field, so audit the rest of the form against guidance on building accessible forms.

An input with type=”search” already exposes the searchbox role. Add a list attribute that points to a datalist and MDN says the role becomes combobox.

Because the implicit role is correct, extra ARIA attributes on a plain bar add nothing. WebAIM ties heavier ARIA use to more detected errors, so a bar should carry none it does not need.

One more trap. An icon-only submit button has no name, so put sr-only text inside it or it reads as an unlabeled control.

Adding Autocomplete and Live Results

A headless combobox suits a short suggestion list that lives in the browser. When results come from a backend, reach for a search API or a hosted index.

The choice matters because autocomplete is easy to ship half right. Baymard’s e-commerce UX benchmark found that only 19% of sites get every implementation detail correct.

Headless UI Combobox

Headless UI is the Tailwind Labs library of unstyled, accessible components for React and Vue, and its Combobox fits a search bar with suggestions. Version 2.0 of the React package shipped in May 2024.

const [query, setQuery] = useState('')
const [selected, setSelected] = useState(null)
const filtered = query === ''
? items
: items.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))

<Combobox value={selected} onChange={setSelected} onClose={() => setQuery('')}>
<ComboboxInput
aria-label="Search"
displayValue={(item) => item?.name}
onChange={(event) => setQuery(event.target.value)}
className="w-full rounded-md border border-gray-300 px-4 py-2"
/>
<ComboboxOptions anchor="bottom" className="rounded-md border border-gray-200 bg-white">
{filtered.map((item) => (
  <ComboboxOption key={item.id} value={item} className="px-4 py-2 data-[focus]:bg-blue-100">
  {item.name}
  </ComboboxOption>
  ))}

</ComboboxOptions>
</Combobox>

Keyboard handling and ARIA wiring are built in, and states surface as data attributes. Filtering is still your job, since the component renders whatever options you pass it.

There’s one catch in React v2. Dropdowns render as modal by default, so page scroll locks and the rest of the page goes inert while the list is open. Pass modal={false} to ComboboxOptions for a navbar search.

Older tutorials still show ui-active: and ui-open: modifiers from the @headlessui/tailwindcss plugin. Headless UI 2.0 added simpler data-* state attributes, such as data-focus, that you style with data variants, so copy from current docs.

The results panel behaves like any dropdown built with Tailwind, anchored under the input. Base UI’s Autocomplete is a comparable unstyled React option, and its documentation includes a Tailwind demo.

Preline combobox for plain HTML

Preline’s combobox plugin is headless and unstyled, driven by data attributes and a small script, so it works without React.

  • apiUrl and apiSearchQuery send queries to an endpoint
  • apiHeaders sets request headers
  • minSearchLength holds back searching until enough characters are typed
  • Client-side filtering switches off by default once apiSearchQuery is set

The documented data options include no debounce setting, although the plugin source wraps its input handler in a debounce helper. Check request volume in the network tab before shipping.

Algolia DocSearch for documentation sites

Algolia dates DocSearch to 2016, when its engineers contributed pull requests to projects such as Bootstrap and jQuery. In @docsearch/react (v5) the DocSearch component takes an appId, an apiKey and an indices array naming the index; v4 and earlier used a single indexName.

A common Tailwind pattern hides the default button and opens the dialog from a custom input styled with the classes from the build section.

The eligibility rules contradict each other a little. Algolia’s announcement says DocSearch is free for all technical documentation sites and blogs, but the DocSearch program page limits the free crawl to open-source documentation and technical blogs. Check closed-source product docs before planning around it.

Debounce, cancellation and keyboard behavior

Element Plus, a Vue UI library, documents a 300 ms default debounce for its autocomplete, which is a sane starting value.

Debounce the input handler so a request to your search API fires after typing pauses. Abort the previous request when a new query starts, so a slow response cannot overwrite a newer one.

  • Skip the native incremental attribute. MDN lists it as a non-standard WebKit and Blink extension.
  • Keep DOM focus in the input while arrow keys move a highlight through the list. The W3C ARIA Authoring Practices Guide does this with aria-activedescendant pointing at the active option.
  • Headless UI handles that pattern. Preline moves DOM focus to the highlighted option instead, and a hand-rolled list has to rebuild the input-focus pattern itself.

Ready-Made Tailwind Search Bar Components

Tailwind Plus, Flowbite, daisyUI, Preline UI and Material Tailwind all ship search inputs, and the right one follows the stack, not the look.

For a single search input, skip them all. Hand-built markup runs a few lines, while a library adds a dependency, a version to track and often a script.

LibraryPricing modelSearch patternsWatch for
Tailwind PlusOne-time license, closed to new customers since September 2026Comboboxes, command palette, Elements autocompleteFull docs sit behind the license
FlowbiteFree package, paid ProSearch input in several styles and sizesInteractive parts need Flowbite’s JavaScript
daisyUIFree pluginInput component class, search built from utilitiesVersion 5 requires Tailwind CSS 4
Preline UIFree core, paid ProHeadless combobox, search markupSeparate CSS and JS per plugin
Material TailwindFree docs components, paid blocksButton, dropdown and Algolia variantsv3 is still a beta on npm, v2 is the stable release

Tailwind Plus is the official option. It was sold as a one-time license with lifetime access, but Tailwind Labs closed sign-ups for new customers when it joined Shopify on September 9, 2026, and existing customers keep their access. Its Elements web components add an autocomplete and a command palette, and Tailwind’s documentation states that Elements needs a commercial license.

Plain HTML with a little JavaScript points to Flowbite or Preline. React with official patterns points to Headless UI, plus Tailwind Plus if you already own a license. daisyUI suits anyone who wants semantic class names on Tailwind v4, and Material Tailwind covers a Material look in React.

daisyUI 4 remains available for projects that cannot move to Tailwind v4 or must support older browsers, according to the daisyUI team.

When a Hand-Built Tailwind Search Bar Is the Wrong Choice

A hand-built bar is the wrong choice when the hard part is the search itself, not the input.

Utility classes style the field. Results, indexing and ranking come from a backend service, and a bar without one searches nothing.

Large or fuzzy data is the first problem. Client-side filtering works only while the whole list can ship to the browser. Algolia’s DocSearch program page says free plans hold up to 10,000 records, while its pricing page lists 50,000 records on the Free plan, so check the current limit before deciding whether a large documentation set needs a paid plan.

Content that isn’t documentation is the second. The free DocSearch program crawls documentation and technical blogs, so product catalogs or app data need your own Algolia account, and removing the Algolia logo disqualifies a site from the free program.

Headless UI’s Combobox targets React and Vue, so a static HTML page needs Preline, Flowbite or a native datalist instead.

Older browsers matter too. Tailwind v4.0 targets Safari 16.4, Chrome 111 and Firefox 128 and does not work in older browsers, per the upgrade guide. Projects that must support them stay on v3.4.

For the React route, get Tailwind CSS running inside a React project before adding Headless UI.

When a native datalist is enough

A short, static suggestion list needs no library. Add a list attribute to the search input, point it at a datalist, and keep the same Tailwind classes on the input.

<input type="search" name="q" list="topics" aria-label="Search" class="w-full rounded-md border border-gray-300 px-4 py-2">
<datalist id="topics">
  <option value="dark mode"></option>

  <option value="focus ring"></option>
</datalist>
  • Works with no JavaScript
  • Suggestions are hints, so users can still type anything (MDN)
  • The browser draws the option list, which limits styling

Tailwind Plus describes its <el-autocomplete> element as behaving like a native datalist with more control over styling. That is the trade: a few lines of HTML against a styled, licensed component that new customers can no longer buy.

Tailwind Search Bar FAQ

Can I use a Tailwind search bar with plain HTML and a CDN?

Yes. Add the @tailwindcss/browser@4 script to the head of the page and the utility classes work with no build step.

Tailwind’s documentation limits that script to development, so compile a static stylesheet before production.

How do I open the search bar with a Cmd+K shortcut?

Attach a keydown listener to the document, check for the Cmd or Ctrl key together with K, call preventDefault, then focus the input or open your results dialog.

Tailwind Plus documents its command palette as typically opened with this shortcut.

How do I add a voice search microphone icon?

Place a microphone button inside the input wrapper, positioned like the search icon, and connect it to the browser’s speech recognition API. Heroicons includes a microphone icon.

MDN marks that API as limited availability, so hide the button where it is unsupported and keep typing as the default path.

Does the same Tailwind markup work in React, Vue and Svelte?

Yes, the class strings carry over unchanged. React needs className and htmlFor in place of class and for, while Vue and Svelte keep standard HTML attributes.

The one catch is @apply inside component style blocks, which needs @reference in Tailwind v4, per the upgrade guide.

What a Tailwind Search Bar Needs Behind It

A Tailwind search bar needs a search source behind it: a backend endpoint, a hosted index or a static list. Without one, the styled input submits a query that nothing answers.

Build from the bottom up so every layer has a working layer beneath it. Pick the search source and its size, submit the form to a results page that works without JavaScript, and add autocomplete last.

The trade-off is a full page load per query until suggestions exist. That slower feel buys a bar that keeps working when a script fails to load.

This position was checked on October 29, 2026 against Tailwind CSS v4.3 and Headless UI v2 for React. A new major release that changes input defaults or Combobox behavior would require rechecking the styling and the autocomplete step.