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.
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 thatadvanceOnClickhas no effect on steps wheredisableActiveInteractionblocks clicks on the element.
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.
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, andonPrevClickhooks 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 calldriverObj.moveNext()ordriverObj.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.
onNextClickandonPrevClickhooks 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.
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.
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;
}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.
destroy() in the hand-off matters for single-page apps, where the old page’s tour would otherwise stay on screen.driverObj.getActiveIndex() + 1, or store a step id and look up its index when resuming, so reordering steps doesn’t break saved progress.waitForElement on the first resumed step keeps the tour from falling back to the centered popover while the page hydrates. See Wait for Elements above.