Skip to main content

Migrating from Driver.js to Tour Kit: Adding Headless Power

Step-by-step guide to replacing Driver.js with userTourKit in a React project. Covers step definitions, popover rendering, highlight migration, callbacks, and multi-page tours.

DomiDex
DomiDexCreator of Tour Kit
April 7, 20269 min read
Share
Migrating from Driver.js to Tour Kit: Adding Headless Power

Migrating from Driver.js to Tour Kit: adding headless power

Driver.js is a lightweight, dependency-free library for creating product tours and element highlights. It ships at roughly 5KB gzipped and works in any JavaScript framework. If your project has outgrown its popover-only UI, imperative API, or lack of React integration, this guide walks you through replacing Driver.js with userTourKit step by step. Every code example compiles against @tourkit/core 0.x and @tourkit/react 0.x.

npm install @tourkit/core @tourkit/react

Why migrate?

Driver.js solves the simple case well. You call driver(), pass some steps, and call drive(). The problems show up when your project needs any of these:

  • Custom UI beyond popovers. Driver.js renders a single popover template. You cannot swap it for a shadcn/ui card, a Radix dialog, or a custom React component without hacking the DOM after render via onPopoverRender.
  • React state integration. Driver.js operates imperatively outside React's component tree. Updating app state when a user completes a step requires manual bridge code between Driver.js callbacks and your React state.
  • Multi-page tours. Driver.js has no built-in support for tours that span multiple routes. You must manually detect page changes, reinitialize the driver, and track which step the user reached.
  • Accessibility. Driver.js popovers lack complete ARIA attributes. GitHub issues document missing aria-expanded, duplicate heading landmarks, and no focus trapping.
  • Analytics. There is no built-in way to track where users drop off, which steps they skip, or completion rates.

userTourKit addresses all five. It is headless -- you provide the UI components, and it handles step sequencing, element positioning, scroll management, keyboard navigation, and persistence. The core package is under 8KB gzipped.

Migration overview

The migration touches five areas. Each section below shows the Driver.js pattern, explains what changes, and gives the Tour Kit equivalent.

  1. Step definitions -- from plain objects to typed TourStep configs
  2. Tour lifecycle -- from imperative drive()/destroy() to React hooks
  3. Popover rendering -- from built-in template to your own components
  4. Highlighting and overlay -- from automatic to configurable
  5. Callbacks and events -- from Driver.js handlers to Tour Kit lifecycle hooks

Step 1: replace step definitions

Driver.js (before)

import { driver } from 'driver.js'
import 'driver.js/dist/driver.css'

const driverObj = driver({
  showProgress: true,
  steps: [
    {
      element: '#sidebar',
      popover: {
        title: 'Navigation',
        description: 'Use the sidebar to switch sections.',
        side: 'right',
        align: 'start',
      },
    },
    {
      element: '#search-input',
      popover: {
        title: 'Search',
        description: 'Find anything in your workspace.',
        side: 'bottom',
        align: 'center',
      },
    },
    {
      element: '#profile-menu',
      popover: {
        title: 'Your profile',
        description: 'Manage settings and preferences.',
        side: 'left',
        align: 'end',
      },
    },
  ],
})

driverObj.drive()

Tour Kit (after)

import { TourProvider } from '@tourkit/react'
import type { TourStep } from '@tourkit/core'

const steps: TourStep[] = [
  {
    id: 'sidebar',
    target: '[data-tour="sidebar"]',
    title: 'Navigation',
    content: 'Use the sidebar to switch sections.',
    placement: 'right-start',
  },
  {
    id: 'search',
    target: '[data-tour="search"]',
    title: 'Search',
    content: 'Find anything in your workspace.',
    placement: 'bottom',
  },
  {
    id: 'profile',
    target: '[data-tour="profile"]',
    title: 'Your profile',
    content: 'Manage settings and preferences.',
    placement: 'left-end',
  },
]

function App() {
  return (
    <TourProvider tourId="onboarding" steps={steps}>
      {/* your app */}
    </TourProvider>
  )
}

What changed

Driver.jsTour KitNotes
element: '#sidebar'target: '[data-tour="sidebar"]'Data attributes are more stable than IDs across refactors
popover.titletitleFlat property, not nested under popover
popover.descriptioncontentAccepts React.ReactNode, not just strings
side: 'right', align: 'start'placement: 'right-start'Combined Floating UI placement string
No id fieldid requiredUsed for persistence, analytics, and conditional logic
CSS import requiredNo CSS importYou control all styling

Tip: Add data-tour attributes to your markup instead of relying on CSS selectors. This decouples tours from your component structure and survives refactors.

Step 2: replace tour lifecycle

Driver.js (before)

// Start
driverObj.drive()

// Navigate
driverObj.moveNext()
driverObj.movePrevious()
driverObj.moveTo(2)

// Stop
driverObj.destroy()

// Check state
driverObj.isActive()
driverObj.getActiveIndex()
driverObj.isLastStep()

Tour Kit (after)

import { useTour } from '@tourkit/react'

function TourControls() {
  const {
    start,
    next,
    prev,
    goTo,
    stop,
    complete,
    isActive,
    currentStepIndex,
    isLastStep,
    isFirstStep,
    totalSteps,
  } = useTour('onboarding')

  return (
    <button onClick={() => (isActive ? stop() : start())}>
      {isActive
        ? `Step ${currentStepIndex + 1} of ${totalSteps}`
        : 'Start tour'}
    </button>
  )
}

What changed

Driver.jsTour KitNotes
driverObj.drive()start()Called from a React component, not globally
driverObj.moveNext()next()Same concept, hook-based
driverObj.moveTo(2)goTo(2)Same concept
driverObj.destroy()stop() or complete()complete() marks tour as finished for persistence
driverObj.isActive()isActiveReactive boolean, triggers re-renders
driverObj.getActiveIndex()currentStepIndexReactive, no method call needed

The key difference is reactivity. Driver.js methods return values at call time. Tour Kit properties are reactive state -- your component re-renders when they change. No manual polling or callback wiring needed.

Step 3: replace popover rendering

This is the biggest difference between the two libraries. Driver.js renders its own popover. Tour Kit gives you hooks and you render whatever you want.

Driver.js (before)

const driverObj = driver({
  popoverClass: 'my-custom-popover',
  nextBtnText: 'Continue →',
  prevBtnText: '← Back',
  doneBtnText: 'Finish',
  showButtons: ['next', 'previous', 'close'],
  onPopoverRender: (popover, { config, state }) => {
    // Hack: inject custom HTML after render
    const customEl = document.createElement('div')
    customEl.textContent = `Step ${state.activeIndex + 1} of ${config.steps.length}`
    popover.description.appendChild(customEl)
  },
  steps: [/* ... */],
})

Tour Kit (after)

import { useTour } from '@tourkit/react'
import { TourCard, TourCardHeader, TourCardContent, TourCardFooter } from '@tourkit/react'

function CustomTourTooltip() {
  const { currentStep, next, prev, stop, isFirstStep, isLastStep, currentStepIndex, totalSteps } =
    useTour('onboarding')

  if (!currentStep) return null

  return (
    <TourCard>
      <TourCardHeader>
        <h3>{currentStep.title}</h3>
        <button onClick={stop} aria-label="Close tour">×</button>
      </TourCardHeader>
      <TourCardContent>
        <p>{currentStep.content}</p>
      </TourCardContent>
      <TourCardFooter>
        <span>
          Step {currentStepIndex + 1} of {totalSteps}
        </span>
        <div>
          {!isFirstStep && <button onClick={prev}>← Back</button>}
          <button onClick={isLastStep ? stop : next}>
            {isLastStep ? 'Finish' : 'Continue →'}
          </button>
        </div>
      </TourCardFooter>
    </TourCard>
  )
}

You can also skip TourCard entirely and use your own design system components:

import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card'
import { Button } from '@/components/ui/button'

function ShadcnTourTooltip() {
  const { currentStep, next, prev, stop, isFirstStep, isLastStep } = useTour('onboarding')

  if (!currentStep) return null

  return (
    <Card className="w-80 shadow-lg">
      <CardHeader className="flex flex-row items-center justify-between">
        <h3 className="text-sm font-semibold">{currentStep.title}</h3>
        <Button variant="ghost" size="icon" onClick={stop} aria-label="Close tour">
          ×
        </Button>
      </CardHeader>
      <CardContent>
        <p className="text-sm text-muted-foreground">{currentStep.content}</p>
      </CardContent>
      <CardFooter className="flex justify-between">
        {!isFirstStep && (
          <Button variant="outline" size="sm" onClick={prev}>
            Back
          </Button>
        )}
        <Button size="sm" onClick={isLastStep ? stop : next}>
          {isLastStep ? 'Done' : 'Next'}
        </Button>
      </CardFooter>
    </Card>
  )
}

No popoverClass, no onPopoverRender hacks, no DOM manipulation. Your tooltip is a React component with full access to your design tokens.

Step 4: replace highlighting and overlay

Driver.js (before)

const driverObj = driver({
  overlayOpacity: 0.7,
  stagePadding: 10,
  stageRadius: 8,
  animate: true,
  steps: [
    {
      element: '#sidebar',
      popover: { title: 'Sidebar', description: '...' },
    },
  ],
})

// Single-element highlight (no tour)
driverObj.highlight({
  element: '#new-feature',
  popover: { title: 'New!', description: 'Check this out.' },
})

Tour Kit (after)

import { TourProvider } from '@tourkit/react'

function App() {
  return (
    <TourProvider
      tourId="onboarding"
      steps={steps}
      spotlight={{
        enabled: true,
        color: 'rgba(0, 0, 0, 0.7)',
        padding: 10,
        borderRadius: 8,
        animate: true,
      }}
    >
      {/* your app */}
    </TourProvider>
  )
}

Configuration mapping

Driver.jsTour KitNotes
overlayOpacity: 0.7spotlight.color: 'rgba(0,0,0,0.7)'Full color control, not just opacity
stagePadding: 10spotlight.padding: 10Same concept
stageRadius: 8spotlight.borderRadius: 8Same concept
animate: truespotlight.animate: trueRespects prefers-reduced-motion automatically

For per-step overrides, use spotlightPadding and spotlightRadius on individual step definitions:

const steps: TourStep[] = [
  {
    id: 'hero',
    target: '[data-tour="hero"]',
    title: 'Welcome',
    content: 'Start here.',
    spotlightPadding: 20,
    spotlightRadius: 16,
  },
]

Standalone highlighting

Driver.js has driverObj.highlight() for one-off element highlights without a full tour. In Tour Kit, use the @tourkit/hints package instead:

npm install @tourkit/hints
import { Hint } from '@tourkit/hints'

function NewFeatureBadge() {
  return (
    <Hint target="[data-tour='new-feature']" placement="top">
      <div className="rounded bg-blue-500 px-3 py-1 text-sm text-white">
        New! Check this out.
      </div>
    </Hint>
  )
}

Step 5: replace callbacks and events

Driver.js (before)

const driverObj = driver({
  onNextClick: (element, step, { config, state }) => {
    console.log('Moving to next step:', state.activeIndex + 1)
    // Must manually advance
    driverObj.moveNext()
  },
  onPrevClick: (element, step, { config, state }) => {
    driverObj.movePrevious()
  },
  onCloseClick: () => {
    console.log('User closed the tour')
    driverObj.destroy()
  },
  onDestroyStarted: () => {
    if (!confirm('Are you sure you want to exit?')) {
      return // Prevents destruction
    }
    driverObj.destroy()
  },
  onDestroyed: () => {
    console.log('Tour destroyed')
  },
  steps: [
    {
      element: '#step-1',
      popover: { title: 'Step 1', description: '...' },
      onDeselected: (element, step) => {
        console.log('Left step 1')
      },
    },
  ],
})

Tour Kit (after)

Tour Kit splits callbacks into tour-level and step-level hooks:

import type { TourStep, Tour } from '@tourkit/core'

const steps: TourStep[] = [
  {
    id: 'step-1',
    target: '[data-tour="step-1"]',
    title: 'Step 1',
    content: '...',
    onShow: (context) => {
      console.log('Entered step 1')
    },
    onHide: (context) => {
      console.log('Left step 1')
    },
    onBeforeShow: (context) => {
      // Return false to prevent showing this step
      return true
    },
  },
]

// Tour-level callbacks
<TourProvider
  tourId="onboarding"
  steps={steps}
  onStart={(context) => console.log('Tour started')}
  onComplete={(context) => console.log('Tour completed')}
  onSkip={(context) => console.log('Tour skipped')}
  onStepChange={(step, index, context) => {
    console.log(`Moved to step ${index + 1}: ${step.id}`)
  }}
>
  {/* your app */}
</TourProvider>

Callback mapping

Driver.jsTour KitNotes
onNextClickHandle in your UI componentYou own the Next button, call next() directly
onPrevClickHandle in your UI componentYou own the Prev button, call prev() directly
onCloseClickHandle in your UI componentYou own the Close button, call stop() directly
onDestroyStartedonBeforeHide on stepReturn false to prevent transition
onDestroyedonComplete or onSkipSeparate handlers for complete vs skip
onDeselected (step)onHide (step)Fires when leaving a step
onPopoverRenderNot neededYou render the popover yourself
onHighlighted (step)onShow (step)Fires when entering a step

The pattern shift: Driver.js gives you callbacks because it owns the UI. Tour Kit gives you hooks and state because you own the UI. Instead of intercepting onNextClick to add custom logic before advancing, you write your own button that runs your logic and then calls next().

Step 6: add multi-page support

Driver.js has no built-in multi-page support. You would typically destroy the driver on navigation and re-create it on the new page, manually tracking progress in localStorage.

Tour Kit handles this natively:

import { TourProvider } from '@tourkit/react'

const steps: TourStep[] = [
  {
    id: 'dashboard-welcome',
    target: '[data-tour="dashboard"]',
    title: 'Dashboard',
    content: 'This is your main dashboard.',
    route: '/dashboard',
    routeMatch: 'exact',
  },
  {
    id: 'settings-intro',
    target: '[data-tour="settings"]',
    title: 'Settings',
    content: 'Configure your preferences here.',
    route: '/settings',
    routeMatch: 'exact',
  },
]

function App() {
  return (
    <TourProvider
      tourId="onboarding"
      steps={steps}
      persist="localStorage"
    >
      {/* your router and app */}
    </TourProvider>
  )
}

The route property on each step tells Tour Kit which page the step belongs to. When the user navigates to that route, the tour picks up where it left off. The persist option saves progress across page reloads.

For Next.js App Router projects, Tour Kit includes a dedicated router adapter:

import { useAppRouterAdapter } from '@tourkit/react'

Step 7: add features Driver.js does not have

Once you have migrated the basics, Tour Kit gives you access to capabilities that would require significant custom code with Driver.js.

Conditional steps

Show or hide steps based on user state:

const steps: TourStep[] = [
  {
    id: 'admin-panel',
    target: '[data-tour="admin"]',
    title: 'Admin panel',
    content: 'Manage your team from here.',
    when: (context) => currentUser.role === 'admin',
  },
]

Branching tours

Route users to different steps based on their actions:

const steps: TourStep[] = [
  {
    id: 'choose-path',
    target: '[data-tour="role-select"]',
    title: 'What describes you best?',
    content: 'Pick your role to customize the tour.',
    onAction: {
      developer: 'dev-step-1',
      designer: 'design-step-1',
      manager: 'manager-step-1',
    },
  },
]

Keyboard navigation

Built into Tour Kit by default. Users can press Arrow Right/Left to navigate, Escape to close. No configuration needed -- but it is configurable if you want to change the keybindings.

Persistence

Tour Kit tracks which tours a user has completed and which step they reached. This works across page reloads and browser sessions:

<TourProvider
  tourId="onboarding"
  steps={steps}
  persist="localStorage"
/>

Driver.js has no built-in persistence. You would need to write your own localStorage wrapper and wire it into onDestroyed.

Migration checklist

Use this checklist to track your migration:

  • Install @tourkit/core and @tourkit/react
  • Remove driver.js and its CSS import (driver.js/dist/driver.css)
  • Convert step definitions to TourStep[] format
  • Add data-tour attributes to target elements
  • Wrap your app in <TourProvider>
  • Build your tooltip component using useTour() hook
  • Migrate callbacks to step-level hooks (onShow, onHide, onBeforeShow)
  • Configure spotlight/overlay settings
  • Add persist="localStorage" if you need progress persistence
  • Add route properties if you have multi-page tours
  • Test keyboard navigation (Arrow keys, Escape)
  • Test with a screen reader to verify ARIA announcements
  • Remove the driver.js package from package.json

API mapping reference

Complete reference mapping every Driver.js API to its Tour Kit equivalent.

Initialization

Driver.jsTour Kit
import { driver } from 'driver.js'import { TourProvider, useTour } from '@tourkit/react'
import 'driver.js/dist/driver.css'Not needed (you control styles)
const d = driver({ steps })<TourProvider tourId="id" steps={steps}>
d.drive()const { start } = useTour('id') then start()
d.drive(2)start('id', 2)
Driver.jsTour Kit
d.moveNext()next()
d.movePrevious()prev()
d.moveTo(n)goTo(n)
d.destroy()stop() or complete()

State

Driver.jsTour Kit
d.isActive()isActive (reactive)
d.getActiveIndex()currentStepIndex (reactive)
d.getActiveStep()currentStep (reactive)
d.isFirstStep()isFirstStep (reactive)
d.isLastStep()isLastStep (reactive)
d.hasNextStep()!isLastStep
d.hasPreviousStep()!isFirstStep

Configuration

Driver.jsTour Kit
showProgress: trueRender in your component
progressText: '{{current}}/{{total}}'Use currentStepIndex and totalSteps in JSX
nextBtnText: 'Next'Set button text in your component
prevBtnText: 'Back'Set button text in your component
doneBtnText: 'Done'Set button text in your component
popoverClass: 'custom'Use className on your component
overlayOpacity: 0.7spotlight.color: 'rgba(0,0,0,0.7)'
stagePadding: 10spotlight.padding: 10
allowKeyboardControl: trueEnabled by default

Callbacks

Driver.jsTour Kit
onNextClickYour button's onClick handler
onPrevClickYour button's onClick handler
onCloseClickYour button's onClick handler
onPopoverRenderNot needed
onDestroyStartedStep onBeforeHide (return false to block)
onDestroyedonComplete or onSkip on provider
Step onDeselectedStep onHide
Step onHighlightStartedStep onBeforeShow

Wrapping up

The migration from Driver.js to Tour Kit is straightforward for simple tours -- convert step definitions, wrap in a provider, build a tooltip component. The real payoff comes from what you gain after migrating: full control over tour UI through your design system, React-native state management, multi-page support, conditional steps, branching logic, persistence, and keyboard accessibility out of the box. If your project has a component library and needs more than basic popovers, the migration is worth the effort.

npm install @tourkit/core @tourkit/react

Ready to try userTourKit?

$ pnpm add @tour-kit/react