Interactive Tours

Sometimes the best way forward in a tour is the product itself: click the button you are pointing at, not a “Next” in the popover. Two options make this work without any custom hooks:

Both can be set for the whole tour or per step.

Advance on Click

The tour below has advanceOnClick enabled, so you can click the highlighted lines to walk through it.

Advance on Click

import { driver } from "driver.js";
import "driver.js/dist/driver.css";

const driverObj = driver({
  advanceOnClick: true,
  showProgress: true,
  steps: [
    { element: '#pick-plan', popover: { title: 'Pick a Plan', description: 'Click the highlighted card to continue.' } },
    { element: '#billing-toggle', popover: { title: 'Billing', description: 'Clicking advances the tour; the toggle still flips.' } },
    { element: '#checkout-btn', popover: { title: 'Checkout', description: 'Clicking the last element ends the tour.' } },
  ],
});

driverObj.drive();

Tip: For truly click-driven steps, hide the next button with showButtons: ['close'] on the step’s popover so the highlighted element is the only way forward. Note that advanceOnClick has no effect on steps where disableActiveInteraction blocks clicks on the element.

Wait for Elements

Click-driven tours usually lead somewhere new: the click opens a modal or a dropdown whose elements don’t exist yet. Give the next step a waitForElement timeout and the tour waits for the element instead of falling back. In the demo below, pressing next renders the target element about a second later; watch the tour hold the current step, then follow.

Wait for Element

import { driver } from "driver.js";
import "driver.js/dist/driver.css";

const driverObj = driver({
  steps: [
    {
      element: '#open-modal-btn',
      advanceOnClick: true,
      popover: {
        title: 'Open the Modal',
        description: 'Clicking this button opens the modal and moves the tour on.',
        showButtons: ['close'],
      },
    },
    {
      // Rendered on demand: the tour waits up to 5 seconds for it,
      // staying on the previous step in the meantime.
      element: '#modal-confirm',
      waitForElement: 5000,
      popover: {
        title: 'Confirm',
        description: 'This step appeared once the modal rendered.',
      },
    },
  ],
});

driverObj.drive();

If the element never appears, the wait times out into the usual missing-element behavior: the centered fallback popover, or a skip when skipMissingElement is set.

Async Steps

When a step needs to load data or render something before the tour can move on, and waitForElement isn’t enough, take over navigation yourself. Override onNextClick, do the work, then call moveNext().

Asynchronous Tour

import { driver } from "driver.js";
import "driver.js/dist/driver.css";

const driverObj = driver({
  showProgress: true,
  steps: [
    {
      popover: {
        title: 'First Step',
        description: 'This is the first step. Next element will be loaded dynamically.'
        // By passing onNextClick, you can override the default behavior of the next button.
        // This will prevent the driver from moving to the next step automatically.
        // You can then manually call driverObj.moveNext() to move to the next step.
        onNextClick: () => {
          // .. load element dynamically
          // .. and then call
          driverObj.moveNext();
        },
      },
    },
    {
      element: '.dynamic-el',
      popover: {
        title: 'Async Element',
        description: 'This element is loaded dynamically.'
      },
      // onDeselected is called when the element is deselected.
      // Here we are simply removing the element from the DOM.
      onDeselected: () => {
        // .. remove element
        document.querySelector(".dynamic-el")?.remove();
      }
    },
    { popover: { title: 'Last Step', description: 'This is the last step.' } }
  ]
});

driverObj.drive();

Note: By overriding onNextClick, and onPrevClick hooks you control the navigation of the driver. This means that user won’t be able to navigate using the buttons and you will have to either call driverObj.moveNext() or driverObj.movePrevious() to navigate to the next/previous step.

You can use this to implement custom logic for navigating between steps. This is also useful when you are dealing with dynamic content and want to highlight the next/previous element based on some logic.

onNextClick and onPrevClick hooks can be configured at driver level as well as step level. When configured at the driver level, you control the navigation for all the steps. When configured at the step level, you control the navigation for that particular step only.

Multi-Page Tours

A driver instance lives within a single page: a full page load destroys it, and even in single-page apps the next route’s elements don’t exist while the current route is showing. The reliable pattern for tours that span navigation is to treat the tour as resumable: persist the step to resume at, let the app navigate normally, and start a fresh tour from the saved step on the next page.

Saving Progress Before Navigating

Define the tour in a function so every page can create it. On the step whose click navigates away, use advanceOnClick together with an onNextClick override: instead of moving to a step that lives on another page, save where to resume and tear the tour down. The link’s own click keeps working, so navigation happens naturally.

import { driver } from "driver.js";
import "driver.js/dist/driver.css";

const TOUR_KEY = "app-tour-step";

export function createTour() {
  let isNavigating = false;

  const driverObj = driver({
    steps: [
      {
        element: "#reports-nav",
        popover: { title: "Reports", description: "Your reports live here." },
      },
      {
        element: "#settings-link",
        // Clicking the highlighted link continues the tour and navigates.
        advanceOnClick: true,
        popover: {
          title: "Open Settings",
          description: "Click this link to continue on the settings page.",
          showButtons: ["close"],
          onNextClick: () => {
            // The next step lives on another page: save where to resume
            // and let the click's navigation happen naturally.
            isNavigating = true;
            localStorage.setItem(TOUR_KEY, "2");
            driverObj.destroy();
          },
        },
      },
      // These steps target elements on the settings page. waitForElement
      // absorbs the rendering delay after a client-side route change.
      {
        element: "#profile-form",
        waitForElement: 5000,
        popover: { title: "Your Profile", description: "Update your details here." },
      },
      {
        element: "#save-settings",
        popover: { title: "Save", description: "Don't forget to save your changes." },
      },
    ],
    onDestroyed: () => {
      // Finished or exited: clear the saved progress so the tour does not
      // come back on the next visit. Skipped for the hand-off destroy above.
      if (!isNavigating) {
        localStorage.removeItem(TOUR_KEY);
      }
    },
  });

  return driverObj;
}

Resuming on the Next Page

On every page load (or in your router’s mount hook), check for saved progress and resume from it:

const savedStep = localStorage.getItem(TOUR_KEY);
if (savedStep !== null) {
  createTour().drive(Number(savedStep));
}

drive() accepts the step index to start from, so the tour picks up exactly where it left off.

Notes