# Interactive Tours

> Build Driver.js tours that react to your app: advance when users click the highlighted element, wait for elements that render on demand, run async logic between steps, and continue across pages.

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:

- `advanceOnClick` advances the tour when the highlighted element is clicked, exactly as if the next button was pressed. The element's own click behavior still runs.
- `waitForElement` makes a step wait up to the given number of milliseconds for its element to appear, staying on the current step with its popover hidden in the meantime.

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.

```js
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.

```js
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()`.

```js
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.

```js
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:

```js
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

- The pattern works the same for full page loads and client-side routing. On a full load the browser tears everything down anyway; the explicit `destroy()` in the hand-off matters for single-page apps, where the old page's tour would otherwise stay on screen.
- The saved value here is a hardcoded index for clarity. In a larger tour, compute it with `driverObj.getActiveIndex() + 1`, or store a step id and look up its index when resuming, so reordering steps doesn't break saved progress.
- If the destination page renders its content asynchronously, `waitForElement` on the first resumed step keeps the tour from falling back to the centered popover while the page hydrates. See [Wait for Elements](#wait-for-elements) above.
