Advanced - Make Your Locator Mobile Responsive | Yext Hitchhikers Platform

What You’ll Learn

In this section, you will:

  • Adjust the stying of your StoreLocator component to make it mobile responsive

Overview

Many users are likely going to be using your locator on their mobile devices. Unfortunately, things don’t look too great on smaller screens right now…

In this unit, you’ll make your locator mobile responsive:


Responsive Design with Tailwind

Tailwind Responsive Design makes it possible for you to change how your Locator behaves depending on the size of the user’s viewport. Tailwind responsive utility variants use CSS media queries under the hood to apply different styling based on the size of the viewport.

For example, let’s say that you want the number of columns in a grid container to change based on how wide the screen is; you can achieve that with something like this:

<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
  {/* grid elements would go here */}
</div>

Based on the Tailwind classes in this example:

  • If the screen is less than 768 pixels wide, the grid will display 2 columns
  • If greater than 768 pixels, but fewer than 1280 pixels, the grid will display 3 columns
  • If greater than 1280 pixels, the grid will display 4 columns

Install Headless UI

Before you begin, you need to install Headless UI React :

npm i @headlessui/react

Headless UI are series of UI components designed to work with Tailwind. We will be using the Switch component in this module to toggle between the search results and the map on mobile.

1. Make the Map and Search Results Mobile Responsive

In this step, you’re going to change the Tailwind classes in your JSX in order to make the search results and map responsive to changes in the width in the viewport. You’re also going to add the Headless UI Switch component that will be used to overlay the results on top of the map rather than to the the left when it is toggled.

You’re also going to add the classNames utility that will allow you to adjust the styling of your components based on changes to variables in your components.

Start by updating your imports to account for the Switch and classNames:

// src/components/StoreLocator.tsx

import {
  MapboxMap,
  FilterSearch,
  OnSelectParams,
  VerticalResults,
  getUserLocation,
  OnDragHandler,
} from "@yext/search-ui-react";
import { useEffect, useState } from "react";
import { BiLoaderAlt } from "react-icons/bi";
import {
  Matcher,
  SelectableStaticFilter,
  useSearchActions,
  useSearchState,
} from "@yext/search-headless-react";
// Mapbox CSS bundle
import "mapbox-gl/dist/mapbox-gl.css";
import LocationCard from "./LocationCard";
import MapPin from "./MapPin";
import { LngLat, LngLatBounds } from "mapbox-gl";
import { Switch } from "@headlessui/react";          // New
import classNames from "classnames";                 // New

// component code...

Then, add the showResults state variable that will be used in conjunction with the switch to track whether the results or map should be visible on mobile:

// src/components/StoreLocator.tsx

// imports...

const StoreLocator = (): JSX.Element => {
  const [showResults, setShowResults] = useState(true);

  // other code...

}

Finally, update your JSX with new Tailwind utility classes:

// src/components/StoreLocator.tsx

// imports...

const StoreLocator = (): JSX.Element => {
	
  // other code...
  return (
    <>
      <div className="relative flex h-[calc(100vh-210px)] flex-col border md:flex-row ">
        {initialSearchState !== "complete" && (
          <div className="absolute z-20 flex h-full w-full items-center justify-center bg-white opacity-70">
            <BiLoaderAlt className="animate-spin " size={64} />
          </div>
        )}
        <div className="flex w-full flex-col overflow-y-auto md:w-1/3">
          <FilterSearch
            onSelect={handleFilterSelect}
            placeholder="Find Locations Near You"
            searchFields={[
              {
                entityType: "location",
                fieldApiName: "builtin.location",
              },
            ]}
          />
          <div className="z-[5] flex w-full justify-center space-x-2 bg-zinc-100 py-4 shadow-lg md:hidden">
            <p>Map</p>
            <Switch
              checked={showResults}
              onChange={setShowResults}
              className={`${
                showResults ? "bg-orange" : "bg-zinc-300"
              } relative inline-flex h-6 w-11 items-center rounded-full`}
            >
              <span
                aria-hidden="true"
                className={`${
                  showResults ? "translate-x-6 " : "translate-x-1 bg-orange"
                } inline-block h-4 w-4 transform rounded-full bg-white transition`}
              />
            </Switch>
            <p>Results</p>
          </div>
          <div
            className={classNames(
              "absolute top-[100px] bg-white left-0 right-0 bottom-0 overflow-hidden overflow-y-auto md:static",
              { "z-[5]": showResults }
            )}
          >
            {resultCount > 0 && (
              <VerticalResults CardComponent={LocationCard} />
            )}
            {resultCount === 0 && initialSearchState === "complete" && (
              <div className="flex items-center justify-center">
                <p className="pt-4 text-2xl">No results found for this area</p>
              </div>
            )}
          </div>
        </div>
        <div className="relative h-[calc(100vh-310px)] w-full md:h-full md:w-2/3">
          <MapboxMap
            mapboxAccessToken={YEXT_PUBLIC_MAPBOX_API_KEY || ""}
            PinComponent={MapPin}
            onDrag={handleDrag}
          />
          {showSearchAreaButton && (
            <div className="absolute top-10 left-0 right-0 flex justify-center">
              <button
                onClick={handleSearchAreaClick}
                className="rounded-2xl border bg-white py-2 px-4 shadow-xl"
              >
                <p>Search This Area</p>
              </button>
            </div>
          )}
        </div>
      </div>
    </>
  );
}

Let’s review what you just added:

  • flex-col and md:flex-row were added to the outer most div. This means that the flex box will be laid out as a column on smaller screens and a column on larger ones.
  • w-1/3 was replaced with md:w-1/3 and w-full was added to the div that contains FilterSearch, VerticalResults, and the Switch. The container will take up the entire width of its parent on mobile but only 1/3rd of its parent (like before) on larger screens.
  • The Switch was added to toggle between the results and map on mobile sized screens.
  • VerticalResults has been wrapped in a div that positions the search results right below the FilterSearch bar and Switch on mobile by using absolute positioning. It will remain statically positioned on larger screens. Results visible when showResults equals true as this will toggle the z-index of the container.
  • The div that contains the MapboxMap replaced w-2/3 with md:w-2/3 and added h-[calc(100vh-310px)], w-full, and md:h-full. The container will take up the entire width of its parent on mobile but only 2/3rds of its parent container (like before) on larger screens. Since you need to reduce the height of the map to account for the filter search and switch containers on mobile, the map container needs to be set accordingly on mobile and take up the full height of its container on larger screens.

If your site isn’t already running, start it back up with npm run dev and go to the locator page. After the map loads for the first time, shrink your viewport and check out how things look on a mobile sized screen.

Try toggling between Map and Results while testing out some of the features you added in previous units.

Finally, expand the viewport back to normal size and see how the locator adjusts accordingly.

unit Quiz
+20 points
Daily Quiz Streak Daily Quiz Streak: 0
Quiz Accuracy Streak Quiz Accuracy Streak: 0
    Error Success Question 1 of 1

    What breakpoints does the locator use to be mobile responsive?

    High five! ✋

    You've already completed this quiz, so you can't earn more points.You completed this quiz in 1 attempt and earned 0 points! Feel free to review your answers and move on when you're ready.
1st attempt
0 incorrect
Feedback