By following this guide, you will build a fully functional, accessible product onboarding tour using Driver.js 2.x inside a Next.js 15 app — no paid third-party service required. The whole implementation takes about 60–90 minutes and produces a tour that is keyboard-navigable, mobile-friendly, and resumable across sessions.

What You'll Build

  • A multi-step onboarding tour that highlights specific UI elements with animated popovers
  • Session persistence using localStorage so the tour only shows once per user
  • A "Restart Tour" button users can trigger manually from a help menu
  • Keyboard and screen-reader accessible tour steps that meet WCAG 2.2 AA standards
  • A clean TypeScript integration pattern that works in any React 18+ project

Prerequisites

  • Node.js 20+ and pnpm 9 (or npm 10)
  • A working Next.js 15 project (App Router)
  • Basic familiarity with React hooks and TypeScript
  • Driver.js 2.3.1 or later (released March 2026)

Step 1: Install Driver.js

Why this version specifically?

Driver.js 2.x introduced a full TypeScript rewrite, a Popover API-compatible overlay, and native support for shadow DOM elements. Versions before 2.0 use a deprecated highlight API that conflicts with Next.js 15's Strict Mode.

pnpm add [email protected]
# or
npm install [email protected]

Import the required CSS inside your root layout or a global stylesheet:

// app/layout.tsx
import 'driver.js/dist/driver.css';

Expected result: Running your dev server should show no console errors. The CSS file is ~6 KB gzipped and adds no render-blocking overhead on modern browsers.

Common pitfall: If you see a document is not defined error during build, Driver.js is being imported at the module level in a Server Component. Move all Driver.js code into a Client Component (see Step 2).

Step 2: Create the Tour Hook

Why isolate tour logic in a custom hook?

Keeping tour logic in a hook separates it from your UI components. It also makes the tour easy to reset, extend, or test independently.

Create hooks/useOnboardingTour.ts:

'use client';

import { useEffect, useCallback } from 'react';
import { driver, type DriveStep } from 'driver.js';

const TOUR_KEY = 'onboarding_tour_completed';

const steps: DriveStep[] = [
  {
    element: '#dashboard-overview',
    popover: {
      title: 'Your Dashboard',
      description: 'See all your key metrics at a glance here.',
      side: 'bottom',
      align: 'start',
    },
  },
  {
    element: '#nav-reports',
    popover: {
      title: 'Reports',
      description: 'Generate detailed reports and export them as CSV or PDF.',
      side: 'right',
    },
  },
  {
    element: '#action-create',
    popover: {
      title: 'Create New',
      description: 'Start a new project or record from here.',
      side: 'left',
    },
  },
];

export function useOnboardingTour() {
  const startTour = useCallback(() => {
    const driverObj = driver({
      showProgress: true,
      animate: true,
      allowClose: true,
      overlayOpacity: 0.55,
      steps,
      onDestroyStarted: () => {
        localStorage.setItem(TOUR_KEY, 'true');
        driverObj.destroy();
      },
    });
    driverObj.drive();
  }, []);

  useEffect(() => {
    const completed = localStorage.getItem(TOUR_KEY);
    if (!completed) {
      // Small delay lets the DOM fully paint before highlighting
      const timeout = setTimeout(startTour, 600);
      return () => clearTimeout(timeout);
    }
  }, [startTour]);

  return { startTour };
}

Expected result: The tour auto-launches once on first load and writes a flag to localStorage on close. Subsequent visits skip the tour automatically.

Pro tip: Replace localStorage with a user profile flag in your database if you want cross-device persistence. For a SaaS app serving users in Australia or Canada, this is especially important for users who log in across devices.

Step 3: Add Element IDs to Your UI

What if an element doesn't exist when the tour runs?

Driver.js 2.x throws a non-breaking console warning when it cannot find a target element and skips that step. Add the correct id attributes to your JSX so each step resolves.

// app/dashboard/page.tsx

export default function DashboardPage() {
  return (
    

Welcome back

{/* metrics cards */}
); }

Common pitfall: Avoid reusing these IDs elsewhere in the document. Duplicate IDs cause Driver.js to target the first matching element, which may not be visible.

Step 4: Wire the Hook Into Your Layout

Create a thin Client Component wrapper so the hook can run in the App Router:

// components/OnboardingTour.tsx
'use client';

import { useOnboardingTour } from '@/hooks/useOnboardingTour';

export function OnboardingTour() {
  useOnboardingTour();
  return null; // renders nothing — side-effect only
}

Add it to your dashboard layout:

// app/dashboard/layout.tsx
import { OnboardingTour } from '@/components/OnboardingTour';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      
      {children}
    
  );
}

Expected result: Load the dashboard in a private browser window. The tour launches automatically after 600 ms, highlights each element in sequence, and marks itself complete on close.

Step 5: Add a "Restart Tour" Trigger

When should users be able to replay the tour?

Always. Users who dismiss the tour early, or who return after months away, benefit from being able to replay it. Expose a trigger in your Help menu or Settings page.

// components/HelpMenu.tsx
'use client';

import { useOnboardingTour } from '@/hooks/useOnboardingTour';

export function HelpMenu() {
  const { startTour } = useOnboardingTour();

  function handleRestart() {
    localStorage.removeItem('onboarding_tour_completed');
    startTour();
  }

  return (
    
  );
}

Pro tip: Log a custom analytics event (e.g. via PostHog or Mixpanel) when handleRestart fires. High restart rates often signal that users are confused — good signal for your product team.

Step 6: Customise Popover Styles to Match Your Brand

Driver.js 2.x exposes CSS custom properties for theming. Override them in your global stylesheet:

/* app/globals.css */
:root {
  --driver-popover-bg-color: #1a1a2e;
  --driver-popover-text-color: #f5f5f5;
  --driver-popover-title-color: #7c3aed;
  --driver-btn-next-bg: #7c3aed;
  --driver-btn-next-color: #ffffff;
  --driver-btn-prev-bg: transparent;
  --driver-btn-prev-border: 1px solid #7c3aed;
  --driver-btn-prev-color: #7c3aed;
  --driver-overlay-color: rgba(0, 0, 0, 0.55);
}

Expected result: The tour popovers now match your design system's colour palette without modifying the Driver.js source. This approach survives package updates cleanly.

Common pitfall: If your app uses a dark mode toggle, listen for the prefers-color-scheme media query and update the CSS variables accordingly with a second rule block.

Step 7: Test Accessibility

Does Driver.js meet WCAG 2.2 AA out of the box?

Driver.js 2.x manages focus automatically, trapping it inside the popover during each step. Keyboard users can navigate with Tab, advance with , and close with Escape. Run axe DevTools (browser extension) against each tour step to confirm no violations remain in your specific implementation.

Additional checks to run:

  • Confirm each popover's title renders as an h6 with a visible focus ring
  • Verify overlay contrast ratio is at least 3:1 against the highlighted element
  • Test on iOS Safari 17+ and Android Chrome 125+ — both used heavily by SMB teams in Singapore and the US

At Lenka Studio, accessibility testing is a standard part of every UI/UX delivery — not an afterthought. Catching these issues before launch saves significant rework time.

Step 8: Measure Tour Completion Rates

Wire up Driver.js lifecycle callbacks to your analytics provider: