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:
- Welcome: Brief intro, sets expectations ("~2 minutes")
- Search panel: How to find systems
- Set origin: Click a system, use context menu
- Set destination: Same flow, different system
- Route panel: Where results appear
- Calculate button: Trigger the pathfinding
- Route display: Reading the results
- Jump range: Adjusting ship capability
- 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
- Keep it short: 9 steps is our maximum. Each step must justify its existence.
- Delay the start: Wait 1-2 seconds for the app to settle before triggering.
- Respect "no thanks": Store both completion AND skip states to avoid re-triggering.
- Theme it: A jarring white popover on a dark app breaks immersion.
- Provide escape hatches: Help menu, keyboard shortcut, exit confirmation.
Clear your EF-Map localStorage (localStorage.clear() in DevTools), refresh, and the tour will start automatically. Or press Shift + ? anytime.
Related Posts
- Refactoring App.tsx: Custom Hooks - How we extracted tour logic as a custom hook
- Embed Guide: Partner Integration - Another onboarding surface for partners
- Anonymous Usage Analytics - How we track tour engagement
- Smart Gate Routing - The feature the tour teaches users to discover