← Back to Blog

Quick Tour: Interactive Onboarding with Driver.js

New users were missing core features. Our analytics showed that 60% of first-time visitors never discovered the routing panel. They'd look at the pretty star map, rotate it a few times, then leave. We needed to teach them without interrupting.

Choosing Driver.js

We evaluated several guided tour libraries:

Library Size Verdict
Intro.js ~30KB Heavy, dated styling
Shepherd.js ~40KB Feature-rich but complex setup
Driver.js ~5KB Lightweight, modern API, easy theming
React Tour ~15KB React-specific, limited customization

Driver.js won for its small bundle size and straightforward API. At ~5KB gzipped, it added negligible overhead.

The 9-Step Tour

We designed a focused tour covering the essential "plan a route" workflow:

  1. Welcome: Brief intro, sets expectations ("~2 minutes")
  2. Search panel: How to find systems
  3. Set origin: Click a system, use context menu
  4. Set destination: Same flow, different system
  5. Route panel: Where results appear
  6. Calculate button: Trigger the pathfinding
  7. Route display: Reading the results
  8. Jump range: Adjusting ship capability
  9. Complete: Encouragement to explore more

Implementation

// tour.ts
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';

export function createRoutingTour() {
  const driverObj = driver({
    showProgress: true,
    allowClose: true,
    overlayColor: 'rgba(0, 0, 0, 0.75)',
    popoverClass: 'ef-map-tour-popover',
    
    steps: [
      {
        popover: {
          title: 'Welcome to EF-Map! 🌌',
          description: 'Let\'s learn to plan routes across EVE Frontier\'s galaxy. This takes about 2 minutes.',
          side: 'center',
          align: 'center',
        }
      },
      {
        element: '#search-panel',
        popover: {
          title: 'Find Systems',
          description: 'Type a system name to search. Try "Jita" or any system you know.',
          side: 'right',
          align: 'start',
        }
      },
      {
        element: '#context-menu-origin',
        popover: {
          title: 'Set Your Starting Point',
          description: 'Right-click any system and select "Set as Origin" to mark your start.',
          side: 'bottom',
          align: 'start',
        }
      },
      // ... remaining steps
    ],
    
    onDestroyStarted: () => {
      if (!driverObj.hasNextStep()) {
        // Tour completed
        localStorage.setItem('ef-map-tour-completed', 'true');
        driverObj.destroy();
      } else {
        // User clicked X mid-tour
        showExitConfirmation(driverObj);
      }
    }
  });
  
  return driverObj;
}

The Exit Confirmation Modal

When users click the X to close the tour early, we show a themed modal instead of just dismissing:

// tour.ts
function showExitConfirmation(driverObj: Driver) {
  const modal = document.createElement('div');
  modal.className = 'ef-tour-exit-modal';
  modal.innerHTML = `
    <div class="ef-tour-exit-content">
      <h3>Leave the tour?</h3>
      <p>You can restart it anytime from the Help menu.</p>
      <div class="ef-tour-exit-buttons">
        <button class="ef-tour-btn-secondary" data-action="continue">
          Continue Tour
        </button>
        <button class="ef-tour-btn-primary" data-action="exit">
          Exit Tour
        </button>
      </div>
    </div>
  `;
  
  document.body.appendChild(modal);
  
  modal.addEventListener('click', (e) => {
    const action = (e.target as HTMLElement).dataset.action;
    if (action === 'exit') {
      localStorage.setItem('ef-map-tour-skipped', 'true');
      driverObj.destroy();
    }
    modal.remove();
  });
}

Theming to Match EF-Map

Driver.js uses simple CSS classes. We overrode defaults to match our dark theme:

/* tour.css */
.ef-map-tour-popover {
  background: #1a1d2e;
  border: 1px solid rgba(138, 180, 248, 0.3);
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}

.ef-map-tour-popover .driver-popover-title {
  color: #8ab4f8;
  font-size: 1.2rem;
  font-weight: 600;
}

.ef-map-tour-popover .driver-popover-description {
  color: #bdc1c6;
  font-size: 1rem;
  line-height: 1.6;
}

.ef-map-tour-popover .driver-popover-progress-text {
  color: #9aa0a6;
  font-size: 0.85rem;
}

.ef-map-tour-popover button.driver-popover-next-btn {
  background: #8ab4f8;
  color: #0b0f17;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
}

.ef-map-tour-popover button.driver-popover-prev-btn {
  background: transparent;
  color: #8ab4f8;
  border: 1px solid rgba(138, 180, 248, 0.3);
}

Triggering the Tour

We offer three entry points:

1. First Visit (Automatic)

// App.tsx
useEffect(() => {
  const hasSeenTour = localStorage.getItem('ef-map-tour-completed') ||
                      localStorage.getItem('ef-map-tour-skipped');
  
  if (!hasSeenTour) {
    // Delay to let the app render first
    setTimeout(() => {
      const tour = createRoutingTour();
      tour.drive();
    }, 1500);
  }
}, []);

2. Help Menu (Manual)

// HelpMenu.tsx
<button onClick={() => {
  const tour = createRoutingTour();
  tour.drive();
}}>
  Start Quick Tour
</button>

3. Keyboard Shortcut

// useKeyboardShortcuts.ts
useEffect(() => {
  const handleKeydown = (e: KeyboardEvent) => {
    if (e.key === '?' && e.shiftKey) {
      const tour = createRoutingTour();
      tour.drive();
    }
  };
  window.addEventListener('keydown', handleKeydown);
  return () => window.removeEventListener('keydown', handleKeydown);
}, []);

Persistence and Analytics

We track tour engagement with our existing analytics:

// tour.ts callbacks
onNextClick: (element, step, options) => {
  track({ type: 'tour_step_completed', step: step.index });
},

onDestroyStarted: () => {
  if (driverObj.hasNextStep()) {
    track({ type: 'tour_abandoned', step: driverObj.getActiveIndex() });
  } else {
    track({ type: 'tour_completed' });
  }
}

Results after 30 days:

Metric Before Tour After Tour
Users who calculate a route 23% 58%
Tour completion rate 71%
Return visits (within 7 days) 18% 34%

Lessons Learned

  1. Keep it short: 9 steps is our maximum. Each step must justify its existence.
  2. Delay the start: Wait 1-2 seconds for the app to settle before triggering.
  3. Respect "no thanks": Store both completion AND skip states to avoid re-triggering.
  4. Theme it: A jarring white popover on a dark app breaks immersion.
  5. Provide escape hatches: Help menu, keyboard shortcut, exit confirmation.
Try It Yourself

Clear your EF-Map localStorage (localStorage.clear() in DevTools), refresh, and the tour will start automatically. Or press Shift + ? anytime.

Related Posts

onboardingdriver.jsguided tourux designuser educationinteractive tutorialeve frontierproduct tour