So before anything else, this article needed a correction. The version that sat here since November described a nine step tour, a table comparing tour libraries by bundle size, and a set of before and after conversion numbers. I never ran that comparison and I have no way of measuring those numbers, so all of it is gone. What is actually in the repo is a five step tour built on driver.js, and it is switched off by default. That turned out to be the more useful thing to write about.
Quick context if you have landed here from a search for driver.js rather than for the game. EF-Map is a free interactive 3D map of EVE Frontier, 24,026 star systems drawn in the browser with Three.js, and the thing most people come for is route planning. Type an origin, type a destination, set your jump range, get a path back. The tour exists because the routing panel is not the first thing your eye goes to when there is a galaxy spinning behind it.
Picking the library, or not really picking it
The library is driver.js, version 1.3.6, and I should be honest that I never did a proper bake off. It came up, it was small, the API was one function call taking an array of steps, and I had something working the same evening. That is the entire evaluation. If you are choosing between driver.js and intro.js or Shepherd today I cannot tell you which is better, only that since November driver.js has not been the thing that broke. It is a plain static import in App.tsx, which means it ships to everyone even though hardly anyone sees the tour, and I have not got round to making that lazy.
The tour is five steps. It opens the routing panel and shows a centred card saying this takes about twenty seconds and you can press escape. Then it highlights the origin field and fills in an example system for you, E75-TS7. Then it highlights the destination field, fills in ES4-0L5, and actually calculates the route in front of you at 60 light years of jump range, optimising for fuel. Then it points at the Copy button so you know how to get the route out of the browser and into the game. Then a last card about right clicking systems on the map to set them directly. Five steps, one real route, nothing to read.
The bit worth passing on: driver.js does not wait for your app
Step three is the interesting one. driver.js will move to the next step the instant you click next. Your app will not. Step three kicks off a real pathfinding run in a web worker, and step four points at a Copy button that does not exist in the DOM until that route has come back and rendered. If you just wire next straight to moveNext() you get a popover pointing at nothing, or pointing at wherever driver.js falls back to when it cannot measure an element, which looks broken in a way that is hard to explain to a first time user.
So step three intercepts its own next click. It refuses to advance until the calculation has had time to render, then polls for the Copy button every 50 milliseconds until the element exists and has a non zero bounding box, with a two second ceiling after which it gives up and advances anyway rather than trapping you in the step.
const maxWaitMs = 2000;
const pollIntervalMs = 50;
let elapsedMs = 0;
const checkCopyButton = () => {
const el = document.querySelector('[data-tour="p2p-copy"]');
const rect = el?.getBoundingClientRect();
const hasValidBounds = rect && rect.width > 0 && rect.height > 0;
if (hasValidBounds) {
driverObj.moveNext();
} else if (elapsedMs < maxWaitMs) {
elapsedMs += pollIntervalMs;
setTimeout(checkCopyButton, pollIntervalMs);
} else {
// Fallback: advance anyway after max wait
driverObj.moveNext();
}
};
There are also setTimeouts in there I am not proud of. 300 milliseconds before filling a field, another 500 before triggering the calculation, then 2500 before deciding the route has finished rendering. Those are guesses that happen to work rather than measurements, and the honest version would hang off the routing worker's own completion event. The polling loop is the one part that is properly deterministic, and it only got written that way because I tried the timeout version first and watched it point at empty space on a slow load.
The overlay is four rectangles, not one
The other thing worth knowing is that driver.js draws its dimming overlay as four SVG rects surrounding the highlighted element, not as one shape with a hole cut in it. That matters the moment you want the dimming to vary per step. On a map, blacking out the background during the step where the entire point is watching a route draw itself across the map is exactly backwards, so step three drops the overlay to nothing, step four sits at a light 0.25, and everything else uses 0.55. There is no config option for it, you query the class and set opacity on all four.
const OVERLAY_OPACITY = {
normal: 0.55, // Default dimming for most steps
light: 0.25, // Copy button visible, map still contextual
none: 0, // Map fully visible during route calculation
} as const;
function setTourOverlay(mode: OverlayMode): void {
const overlays = document.querySelectorAll('.driver-overlay');
const opacity = OVERLAY_OPACITY[mode];
overlays.forEach((el) => {
(el as SVGElement).style.opacity = String(opacity);
});
}
Theming was more of a fight than I expected
driver.css sets its own font stack on the popover root and an embossed text shadow on the footer buttons, and it loads after the app's own stylesheet, so at equal specificity it wins. Nearly every rule in my tour stylesheet ends up carrying an !important, which I do not love but could not get rid of without something uglier. The accent colour is a separate problem again, because EF-Map lets you change the theme accent at runtime, so the popover title colour, the next button background and the highlight outline are all set as inline styles from inside driver.js's onPopoverRender hook rather than baked into CSS. One small win that saved a lot of markup: setting white-space: pre-line on the description means you can put plain newlines straight into the step text and they render.
The accessibility one you will probably hit too
driver.js supports steps with no element, the centred welcome card kind, and to position those it invents an invisible zero by zero div with the id driver-dummy-element. It then puts aria-haspopup, aria-expanded and aria-controls on it, the same as it would for a real highlighted widget. Those attributes are not valid on a generic div, and both Lighthouse and axe flag it, surfaced as the accessibility tree not being well formed. The dummy is purely positional, so the fix is to strip the three attributes and hide it from the accessibility tree. driver.js reapplies them on every step transition, so it has to run from the per step hooks, and because the attributes get written after the hook returns on animated transitions it also has to run again on the next frame.
function sanitizeDriverDummyElement(): void {
const dummy = document.getElementById('driver-dummy-element');
if (!dummy) return;
dummy.setAttribute('aria-hidden', 'true');
dummy.removeAttribute('aria-haspopup');
dummy.removeAttribute('aria-expanded');
dummy.removeAttribute('aria-controls');
}
There is a singleton guard wrapped round the whole thing as well, because a double click on the tour button used to start a second tour on top of the first and the two of them would fight over the overlay. If a tour is already running the factory hands back a wrapper whose drive() does nothing at all.
Now, the reason it is switched off
The tour does not auto start any more, and the button that launches it is hidden by default. Both of those are deliberate. It was auto starting for first time visitors right up until July, when I turned it off, and the reason is sitting in the code comment where the effect used to be. The tour demonstrates a 60 light year jump, and by that point in the game jump drives were not a capability players actually had. Gates and player built smart gates were the way you travelled. So the very first thing a new visitor saw was a confident demonstration of something they could not do, which is worse than showing them nothing.
What replaced it is a small dismissible intro card that appears a couple of seconds after the map settles, which is a much lower commitment ask, and the tour itself is still there behind a Quick Tour button you can switch back on in Display Settings. It also never runs on phones, because every selector in it targets the desktop layout, and if the app becomes the phone shell mid tour it gets torn down silently without marking itself complete. Completion is one localStorage key, and exiting early through the confirmation modal sets the same key, on the grounds that someone who has said no once should not be asked again.
I do track tour starts. There is a single tour_started event that increments a counter on the worker side, and that is the only tour event that exists, so I can tell you the counter is there but not a completion rate, because nothing measures completion. The old version of this article quoted one. It was invented. If I ever wire up a completion event I will come back and put the real figure here.
So yeah, that is the honest state of it. A tour that works, that I learned a fair amount building, and that almost nobody sees because the product moved and the tour did not move with it. I think that is the actual lesson rather than anything about libraries. A guided tour is a hard coded snapshot of your interface and, if you are building for a game, of the game's rules too, and it rots the moment either one changes. Unlike a stale help page it rots in front of a brand new user in their first thirty seconds. If you are building one, work out in advance who is going to notice when it goes out of date, because in my case nobody did for months. If you have got a tour that survived a couple of years of product change, I would like to hear how you kept it honest.
Related Posts
Making EF-Map Usable on Phones covers the mobile shell that the tour is deliberately never shown in.
Rebuilding the Help System is the other half of teaching people the map, and the half that has aged better than the tour.
Transparency and Client-Side Architecture explains what the usage counters, including tour starts, actually collect.
Half a Million Light-Years Saved: Eleven Months of Usage Data is what the aggregate numbers do show, as opposed to the ones this article used to make up.
Smart Gate Routing is the routing work sitting underneath the panel the tour walks you through.