DEV Community

Cover image for Create an Amazing Interactive Tour in React – A Complete Guide
Hayyan Hami
Hayyan Hami

Posted on • Originally published at hayyaun.ir

Create an Amazing Interactive Tour in React – A Complete Guide

Creating an engaging and interactive tour is a powerful way to onboard users, highlight key features, and enhance the user experience in your React application. 🌟 Whether you’re introducing new functionality, guiding first-time users, or providing contextual tips, a well-designed interactive tour can leave a lasting impression. 💡 In this complete guide, we’ll walk you through the process step by step—from planning and designing your tour to implementing it with best practices. 🛠️ By the end, you’ll have all the tools and knowledge needed to create a polished, user-friendly tour that makes your app shine and keeps your users coming back for more. 🎉

To achieve this, we need to create a Tour component. This component will accept a list of steps and navigate through them sequentially.

Define Interfaces

When defining type interfaces for each step, a query selector is used to locate the area that will be highlighted. Additionally, each step includes a title for the description box and content for the description itself.

// Tour.tsx
export interface ITourStep {
  q: string; // Query selector string
  title: string; // Title for the description box
  content: ReactNode; // Content to be displayed as the description
}

interface IProps {
  items: ITourStep[] | null; // List of tour steps. null indicates inactive state
  onComplete: () => void; // Callback to trigger on tour finished 
}

export default function Tour({ steps, onComplete }: IProps) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Add Elements

There are two key elements: one focuses on highlighting the target area while dimming the surrounding region, and the other is the bubble that displays the title, description, and buttons.

// Tour.tsx

...

export default function Tour({ steps, onComplete }: IProps) {
  const focus = useRef<HTMLDivElement>(null!);
  const bubble = useRef<HTMLDivElement>(null!);
  const content = useRef<HTMLSpanElement>(null!);
  const [index, setIndex] = useState(0); // active index

  ...

  return (
    <>
      <div
        ref={focus}
        className="pointer-events-none fixed left-0 top-0 z-[99] hidden size-56 rounded-lg border-2 border-white/10 opacity-0 shadow-[0_0_0_9999px_#000000f2]"
      />
      <div
        ref={bubble}
        className="fixed left-0 top-0 z-[100] hidden w-64 flex-col gap-3 rounded-xl bg-white p-4 opacity-0"
      >
        <span ref={content} className="text-justify text-sm text-black" />
        <button onClick={onNext} className="text-green-400">
          {!!active && active < items.length ? "Next" : "Done"}
        </button>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Final Code

The final step is to add animations and functionality to the component. This includes displaying step 1 when the steps change, updating the current step when the "Next" button is clicked, and hiding the tour when the steps are set to null. Additionally, we need to ensure the focused area scrolls into view and that the bubble is positioned optimally, adapting to all screen sizes for a responsive design.

// Tour.tsx
"use client";

import { useGSAP } from "@gsap/react";
import clsx from "clsx";
import gsap from "gsap";
import { TextPlugin } from "gsap/all";
import { useRef, useState } from "react";

gsap.registerPlugin(TextPlugin);

export interface ITourStep {
  q: string; // Query selector string
  title: string; // Title for the description box
  content: ReactNode; // Content to be displayed as the description
}

interface IProps {
  items: ITourStep[] | null; // List of tour steps. null indicates inactive state
  onComplete: () => void; // Callback to trigger on tour finished 
}

export default function Tour({ steps, onComplete }: IProps) {
  const focus = useRef<HTMLDivElement>(null!);
  const bubble = useRef<HTMLDivElement>(null!);
  const content = useRef<HTMLSpanElement>(null!);
  const [index, setIndex] = useState(0); // active index

  useGSAP(async () => {
    const inactive = !index || !steps || index > steps.length;

    gsap.to([bubble.current, focus.current], {
      opacity: inactive ? 0 : 1,
      display: inactive ? "non" : "flex",
      duration: 0.7,
    });

    if (inactive) return;
    const step = steps[index - 1];
    const el = document.querySelector(step.q);
    if (!el) return;
    el.scrollIntoView({ block: "center", inline: "start" });
    const { width: bw } = bubble.current.getBoundingClientRect();
    const { width, height: h0, x, y } = el.getBoundingClientRect();
    const [vw, vh] = [window.innerWidth, window.innerHeight];
    const sm = vw < lg;
    const height = h0 > vh ? vh - oy2 : h0;
    const right = x + width / 2 > vw / 2;
    const bigH = height > vh / 2;
    const bx = sm ? -ox : !right ? width + ox2 : -(bw + ox2);
    const by = sm ? (!bigH ? height + oy2 : height / 2 + oy2) : -oy;
    gsap.to(bubble.current, { left: x, top: y, x: bx, y: by });
    const [w, h] = [width + ox2, height + oy2];
    const [fx, fy] = [x - ox, y - oy];
    gsap.to(focus.current, { width: w, height: h, left: fx, top: fy });
    gsap.to(content.current, { text: step.content });
  }, [index, steps]);

  const onNext = () => {
    if (index > steps.length - 1) return setIndex(0);
    setIndex((i) => i + 1);
  };

  return (
    <>
      <div
        ref={focus}
        className="pointer-events-none fixed left-0 top-0 z-[99] hidden size-56 rounded-lg border-2 border-white/10 opacity-0 shadow-[0_0_0_9999px_#000000f2]"
      />
      <div
        ref={bubble}
        className="fixed left-0 top-0 z-[100] hidden w-64 flex-col gap-3 rounded-xl bg-white p-4 opacity-0"
      >
        <span ref={content} className="text-justify text-sm text-black" />
        <button onClick={onNext} className="text-green-400">
          {!!active && active < items.length ? "Next" : "Done"}
        </button>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Usage

const tour: ITourStep[] = [
  {
    q: "#box-1",
    title: "Title 1",
    content: "Content 1",
  },
  {
    q: "#box-2",
    title: "Title 2",
    content: "Content 2",
  },
];

export default function App() {
  const [steps, setSteps] = useState<ITourStep[] | null>(null);

  const onStart = () => setSteps(tour);
  const onComplete = () => setSteps(null);

  return (
    <div id="home">
      <div id="box-1">...</div>
      <div id="box-2">...</div>
      <button onClick={onStart} />
      <Tour steps={steps} onComplete={onComplete} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🎉 Congratulations! 🎉

You’ve successfully built an amazing interactive tour in React! 🚀 By following this guide, you’ve created a user-friendly feature that not only enhances your app's usability but also delivers a delightful experience for your users. From planning the steps to implementing animations and responsive design, you've covered everything needed to make your tour both functional and engaging.

Now it’s time to take your app to the next level. Whether you're onboarding new users or guiding them through advanced features, your interactive tour is ready to shine! ✨

Thank you for reading, and happy coding! 💻🎨

Top comments (0)