DEV Community

Cover image for Using shadcn/ui for an Autocomplete Component
Olaitan Odulaja
Olaitan Odulaja

Posted on

Using shadcn/ui for an Autocomplete Component

Autocomplete is one of those UI elements that feels simple until you try to build one that looks great, feels responsive, and is flexible enough to extend. That’s where shadcn/ui comes in.

Check out the full project here (Still a WIP)

What is shadcn/ui?

shadcn/ui is a component library that brings together the flexibility of headless UI, the utility of Tailwind CSS, and the aesthetics of pre-styled components—all while keeping your codebase in control.

Why Should You Use It?

Here’s what makes it stand out:

  • Open Code: You own the top layer. Easily customize anything without hacking around abstraction.
  • Composition: Components are built with predictable, composable patterns.
  • Distribution: Simple CLI and flat-file system make setup and updates seamless.
  • Beautiful Defaults: Clean, modern styles out of the box.
  • AI-Ready: Because the code is open and modular, it’s easy for LLMs to analyze and suggest improvements.

Installation (Next.js)

pnpm dlx shadcn@latest init
Enter fullscreen mode Exit fullscreen mode

This sets up the base config and connects the CLI to your project.

The Command Component

This is the core of the autocomplete feature. It’s a dynamic, accessible component designed for fuzzy search and filtering.

Install it:

  pnpm dlx shadcn@latest add command  
Enter fullscreen mode Exit fullscreen mode

Import the component and structure your layout.

"use client";
import { useState, useCallback } from "react";
import {
   Command,
   CommandEmpty,
   CommandGroup,
   CommandInput,
   CommandItem,
   CommandList,
   CommandSeparator,
} from "@/components/ui/command";
Enter fullscreen mode Exit fullscreen mode

Create the component

Neighborhood Rating

<>
         <div className="flex flex-col items-center justify-center relative">
            <div className="flex justify-center mt-8">
               <Command
                  className="rounded-lg border shadow-md w-[540px] bg-white"
                  shouldFilter={false}
               >
                  <CommandInput
                     placeholder="Search by address or postcode"
                     defaultValue={userInput}
                     onValueChange={(v) => handleUserInput(v)}
                     className="block w-[480px] h-[48px] -mr-16 text-base text-gray-900"
                  />

                  <CommandList>
                     <CommandEmpty>No results found.</CommandEmpty>
                     <CommandGroup>
                        {predictions.map((prediction) => (
                           <CommandItem
                              key={prediction.placePrediction?.placeId}
                              value={prediction.placePrediction?.placeId}
                              onSelect={(value) => handleSelectedPlace(value)}
                           >
                              {prediction.placePrediction?.text.text}
                           </CommandItem>
                        ))}
                     </CommandGroup>
                     <CommandSeparator />
                  </CommandList>
               </Command>
               <button
                  type="button"
                  className="rounded-r-lg h-[48px] -ml-48 w-48 bg-roofone-green-primary mt-[0.75px] px-4 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
               >
                  Calculate rating
               </button>
            </div>
         </div>
      </>
Enter fullscreen mode Exit fullscreen mode

I used the CommandInput to handle user input and the CommandList to render results from the API call. It is important to not that Shadcn\ui handles state changes for you.

Tip: Use the shouldFilter prop to control search behavior—great for more advanced use cases like server-side filtering or custom ranking.

Create event handlers to handle user input and rendering the results.

   const [predictions, setPredictions] = useState<
      google.maps.places.AutocompleteSuggestion[]
   >([]);

   const handleUserInput = useCallback(
      debounce((input: string) => {
         setUserInput(input);
         fetchPredictions(input);
      }, 1000),
      []
   );

   async function fetchPredictions(input: string) {
      const queryBody = {
         input: input,
         includedRegionCodes: ["uk"],
         includeQueryPredictions: true,
      };
      try {
         const res = await PlacesApi.post("places:autocomplete", queryBody);
         const data = await res.json();
         if (!res.ok) throw new Error("Failed to fetch predictions");
         console.log("received suggestings ->", data.suggestions);
         setPredictions(data.suggestions ?? []);
      } catch (error) {
         console.log(error);
      }
   }

   const handleSelectedPlace = (placeId: string) => {
      const selectedInput = predictions.filter(
         (prediction) => prediction.placePrediction?.placeId === placeId
      );
      setUserInput(String(selectedInput[0].placePrediction?.text.text));
      handleGetSelectedPlaceRating(placeId);
   };
Enter fullscreen mode Exit fullscreen mode

Here's a simple explanation of my code.

  • The prediction state stores the response from the Google Maps API
  • handleUserInput helps rate limit the user input by using the debounce helper function.
  • fetchPredictions takes a string and uses the PlacesApi helper function to GET the predictions and sets the array of predictions into state.
  • handleSelectedPlace uses the CommandItem value which is the place Id to get the text the user selected and then calls a function prop from the parent component.

Key challenged faced: Google Autocomplete Types

I initially found it tricky to properly type Google Maps results but after pair programming with my friend @opeadeyomoye, we were able to find the DefintelyTyped Google Maps Types and it was a live saver!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay