driverjs-guide — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited driverjs-guide (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Driver.js is a lightweight (~5kb gzipped), dependency-free, vanilla TypeScript library for guiding user focus across a web page. It draws an SVG overlay and cuts out a portion above the highlighted element, avoiding z-index and stacking-context issues entirely.
Use cases beyond product tours: contextual form help, feature announcements, focus-shifting overlays, simple modals, and "turn off the lights" effects.
Documentation source: https://driverjs.com
Before writing any Driver.js code, check whether the current project already has driver.js installed:
"driver.js" in package.json (both dependencies and devDependencies).package.json, also check package-lock.json, yarn.lock, or pnpm-lock.yaml for driver.js.from "driver.js" or from 'driver.js' across the codebase to see if it is already in use.Based on results:
yarn.lock, pnpm-lock.yaml, or default to npm).# npm
npm install driver.js
# pnpm
pnpm install driver.js
# yarn
yarn add driver.js<script src="https://cdn.jsdelivr.net/npm/driver.js@latest/dist/driver.js.iife.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/driver.js@latest/dist/driver.css"/>import { driver } from "driver.js";
import "driver.js/dist/driver.css";The CSS import is required for default popover and overlay styling.
import { driver } from "driver.js";
import "driver.js/dist/driver.css";
const driverObj = driver({
showProgress: true,
steps: [
{ element: '.page-header', popover: { title: 'Title', description: 'Description' } },
{ element: '.top-nav', popover: { title: 'Title', description: 'Description' } },
{ element: '.sidebar', popover: { title: 'Title', description: 'Description' } },
{ element: '.footer', popover: { title: 'Title', description: 'Description' } },
]
});
driverObj.drive();const driverObj = driver();
driverObj.highlight({
element: '#some-element',
popover: {
title: 'Title for the Popover',
description: 'Description for it',
},
});Omit the element property entirely to show a centered popover with no highlight cutout:
driverObj.highlight({
popover: {
title: 'Welcome!',
description: 'Let us show you around.',
}
});This also works as a step in a tour (just omit element from that step object).
Driver.js has three configuration layers that can be combined:
driver({...})Step-level settings override driver-level settings for that step.
type Config = {
steps?: DriveStep[];
// Animation and scrolling
animate?: boolean; // default: true
smoothScroll?: boolean; // default: false
// Overlay appearance
overlayColor?: string; // default: "black" (any CSS color)
overlayOpacity?: number; // default: 0.5
// Overlay behavior
allowClose?: boolean; // default: true (close on backdrop click or Esc)
overlayClickBehavior?: "close" | "nextStep"
| ((element?: Element, step: DriveStep, options: { config: Config; state: State; driver: Driver }) => void);
// default: "close"
// Highlight cutout
stagePadding?: number; // default: 10 (px between element and cutout edge)
stageRadius?: number; // default: 5 (border-radius of cutout)
// Keyboard
allowKeyboardControl?: boolean; // default: true (arrow keys, Esc)
// Interaction
disableActiveInteraction?: boolean; // default: false (if true, blocks clicks on highlighted element)
// Popover defaults (can be overridden per step)
popoverClass?: string;
popoverOffset?: number; // default: 10 (px gap between popover and element)
showButtons?: AllowedButtons[]; // default: ["next","previous","close"] for tours, [] for highlight
disableButtons?: AllowedButtons[];
showProgress?: boolean; // default: false
progressText?: string; // default: "{{current}} of {{total}}"
nextBtnText?: string;
prevBtnText?: string;
doneBtnText?: string; // shown on last step instead of nextBtnText
// Hooks (all receive: element?, step, { config, state, driver })
onPopoverRender?: (popover: PopoverDOM, options) => void;
onHighlightStarted?: (element?, step, options) => void;
onHighlighted?: (element?, step, options) => void;
onDeselected?: (element?, step, options) => void;
onDestroyStarted?: (element?, step, options) => void;
onDestroyed?: (element?, step, options) => void;
onNextClick?: (element?, step, options) => void;
onPrevClick?: (element?, step, options) => void;
onCloseClick?: (element?, step, options) => void;
};Critical behavior note on `onNextClick` / `onPrevClick`: When you override these hooks, the default navigation is disabled. You MUST call driverObj.moveNext() or driverObj.movePrevious() yourself inside the handler, otherwise the buttons will appear to do nothing.
Same applies to onCloseClick -- you must call driverObj.destroy() yourself.
These hooks can be set at driver level (applies to all steps) or at step level (applies to that step only). Step-level overrides take precedence.
type Popover = {
title?: string; // supports HTML
description?: string; // supports HTML
// Positioning
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
// Buttons
showButtons?: ("next" | "previous" | "close")[];
disableButtons?: ("next" | "previous" | "close")[];
nextBtnText?: string;
prevBtnText?: string;
doneBtnText?: string;
// Progress
showProgress?: boolean;
progressText?: string; // "{{current}} of {{total}}"
// Styling
popoverClass?: string;
// Hooks
onPopoverRender?: (popover: PopoverDOM, options) => void;
onNextClick?: (element?, step, options) => void;
onPrevClick?: (element?, step, options) => void;
onCloseClick?: (element?, step, options) => void;
};Popover auto-positioning: The popover automatically repositions itself if it does not fit in the viewport with the specified side/align. You do not need to handle edge cases manually.
type DriveStep = {
element?: Element | string | (() => Element);
// CSS selector (first match), DOM element, or function returning one.
// Omit for a floating popover with no highlight.
popover?: Popover;
disableActiveInteraction?: boolean; // default: false
// Per-step hooks
onDeselected?: (element?, step, options) => void;
onHighlightStarted?: (element?, step, options) => void;
onHighlighted?: (element?, step, options) => void;
};Accessible via driverObj.getState() and passed to all hooks:
type State = {
isInitialized?: boolean;
activeIndex?: number;
activeElement?: Element;
activeStep?: DriveStep;
previousElement?: Element;
previousStep?: DriveStep;
popover?: PopoverDOM;
};Passed to onPopoverRender. Use it to manipulate the popover before display:
type PopoverDOM = {
wrapper: HTMLElement;
arrow: HTMLElement;
title: HTMLElement;
description: HTMLElement;
footer: HTMLElement;
progress: HTMLElement;
previousButton: HTMLElement;
nextButton: HTMLElement;
closeButton: HTMLElement;
footerButtons: HTMLElement;
};const driverObj = driver({ /* config */ });
// Tour navigation
driverObj.drive(); // start from step 0
driverObj.drive(4); // start from step 4
driverObj.moveNext();
driverObj.movePrevious();
driverObj.moveTo(4);
// Tour state queries
driverObj.hasNextStep();
driverObj.hasPreviousStep();
driverObj.isFirstStep();
driverObj.isLastStep();
driverObj.getActiveIndex();
driverObj.getActiveStep();
driverObj.getPreviousStep();
driverObj.getActiveElement();
driverObj.getPreviousElement();
driverObj.isActive();
// Runtime mutations
driverObj.refresh(); // recalculate highlight position (useful after DOM changes)
driverObj.setConfig({ /* ... */ }); // update config on the fly
driverObj.getConfig();
driverObj.setSteps([ /* ... */ ]); // replace steps array
// State
driverObj.getState();
// Single highlight (same DriveStep format)
driverObj.highlight({ element: '#el', popover: { title: '...' } });
// Teardown
driverObj.destroy();/* Body classes while driver is active */
.driver-active {} /* always present when active */
.driver-fade {} /* when animate: true */
.driver-simple {} /* when animate: false */
/* Overlay */
.driver-overlay {}
/* Highlighted element */
.driver-active-element {}.driver-popover {}
.driver-popover-arrow {}
.driver-popover-title {}
.driver-popover-description {}
.driver-popover-close-btn {}
.driver-popover-footer {}
.driver-popover-progress-text {}
.driver-popover-prev-btn {}
.driver-popover-next-btn {}When you change the popover background color, you must also update the arrow border color to match. The arrow side is indicated by a class:
.driver-popover-arrow-side-left.driver-popover-arrow { border-left-color: #yourColor; }
.driver-popover-arrow-side-right.driver-popover-arrow { border-right-color: #yourColor; }
.driver-popover-arrow-side-top.driver-popover-arrow { border-top-color: #yourColor; }
.driver-popover-arrow-side-bottom.driver-popover-arrow { border-bottom-color: #yourColor; }Apply a custom class via popoverClass, then scope your CSS under it:
const driverObj = driver({
popoverClass: 'my-theme'
});.driver-popover.my-theme {
background-color: #fde047;
color: #000;
}
.driver-popover.my-theme .driver-popover-title {
font-size: 20px;
}
.driver-popover.my-theme button {
background-color: #000;
color: #fff;
border: 2px solid #000;
border-radius: 6px;
padding: 5px 8px;
font-size: 14px;
text-shadow: none;
}
.driver-popover.my-theme .driver-popover-close-btn {
color: #9b9b9b;
}
.driver-popover.my-theme .driver-popover-close-btn:hover {
color: #000;
}
.driver-popover.my-theme .driver-popover-navigation-btns {
justify-content: space-between;
gap: 3px;
}
/* Match arrow color to background */
.driver-popover.my-theme .driver-popover-arrow-side-left.driver-popover-arrow { border-left-color: #fde047; }
.driver-popover.my-theme .driver-popover-arrow-side-right.driver-popover-arrow { border-right-color: #fde047; }
.driver-popover.my-theme .driver-popover-arrow-side-top.driver-popover-arrow { border-top-color: #fde047; }
.driver-popover.my-theme .driver-popover-arrow-side-bottom.driver-popover-arrow { border-bottom-color: #fde047; }For changes beyond CSS (adding elements, restructuring), use onPopoverRender:
const driverObj = driver({
onPopoverRender: (popover, { config, state }) => {
const btn = document.createElement("button");
btn.innerText = "Go to First";
popover.footerButtons.appendChild(btn);
btn.addEventListener("click", () => driverObj.drive(0));
},
steps: [/* ... */]
});const driverObj = driver({
showProgress: true,
steps: [
{ element: '.header', popover: { title: 'Header', description: 'This is your app header.', side: 'left', align: 'start' } },
{ element: '.sidebar', popover: { title: 'Sidebar', description: 'Navigate here.', side: 'right', align: 'start' } },
{ popover: { title: 'Done!', description: 'You are all set.' } }
]
});
driverObj.drive();Set animate: false to disable the smooth transition between steps:
const driverObj = driver({
animate: false,
showProgress: false,
showButtons: ['next', 'previous', 'close'],
steps: [/* ... */]
});
driverObj.drive();const driverObj = driver({
showProgress: true,
// Optional custom template:
progressText: 'Step {{current}} of {{total}}',
steps: [/* ... */]
});Override onNextClick at the step level to fetch data or render DOM before proceeding. You MUST call driverObj.moveNext() yourself:
const driverObj = driver({
showProgress: true,
steps: [
{
popover: {
title: 'Step 1',
description: 'Next element will be loaded dynamically.',
onNextClick: () => {
// Load or render the element dynamically here
const el = document.createElement('div');
el.className = 'dynamic-el';
el.textContent = 'I was loaded async!';
document.body.appendChild(el);
driverObj.moveNext();
},
},
},
{
element: '.dynamic-el',
popover: { title: 'Async Element', description: 'This was loaded dynamically.' },
onDeselected: () => {
document.querySelector('.dynamic-el')?.remove();
}
},
{ popover: { title: 'Last Step', description: 'Tour complete.' } }
]
});
driverObj.drive();Use onDestroyStarted to intercept exit attempts. You are responsible for calling driverObj.destroy():
const driverObj = driver({
steps: [/* ... */],
onDestroyStarted: () => {
if (!driverObj.hasNextStep() || confirm("Are you sure you want to exit?")) {
driverObj.destroy();
}
},
});
driverObj.drive();Set allowClose: false to block backdrop clicks and Esc key. The user must complete the tour:
const driverObj = driver({
allowClose: false,
steps: [/* ... */]
});
driverObj.drive();const driverObj = driver({
overlayColor: 'red', // any CSS color string
overlayOpacity: 0.7,
});
driverObj.highlight({
element: '#target',
popover: { title: 'Red overlay', description: 'Custom color backdrop.' }
});Control with side and align on each step's popover:
side: "top" | "right" | "bottom" | "left"align: "start" | "center" | "end"12 combinations total. The popover auto-adjusts if it overflows the viewport.
driverObj.highlight({
element: '#el',
popover: {
title: 'Positioned',
description: 'Bottom-start placement.',
side: 'bottom',
align: 'start'
}
});// Show specific buttons
driver({ showButtons: ['next', 'previous'] }); // no close button
// Disable specific buttons
driver({ disableButtons: ['previous'] }); // previous visible but grayed out
// Custom button text
driver({
nextBtnText: '-->',
prevBtnText: '<--',
doneBtnText: 'Finish',
});
// Completely custom buttons via onPopoverRender
driver({
onPopoverRender: (popover, { config, state }) => {
const btn = document.createElement("button");
btn.innerText = "Go to First";
popover.footerButtons.appendChild(btn);
btn.addEventListener("click", () => driverObj.drive(0));
},
steps: [/* ... */]
});const driverObj = driver({
popoverClass: 'form-help-theme',
stagePadding: 0,
onDestroyed: () => document?.activeElement?.blur(),
});
document.getElementById('name').addEventListener('focus', () => {
driverObj.highlight({
element: '#name',
popover: { title: 'Name', description: 'Enter your full name.' },
});
});
document.getElementById('email').addEventListener('focus', () => {
driverObj.highlight({
element: '#email',
popover: { title: 'Email', description: 'We will send a confirmation here.' },
});
});Both title and description accept raw HTML:
driverObj.highlight({
popover: {
description: '<img src="https://example.com/demo.gif" style="width:270px;" /><p>Watch the demo above.</p>',
}
});const driverObj = driver({
overlayClickBehavior: 'nextStep',
steps: [/* ... */]
});Or pass a custom function:
overlayClickBehavior: (element, step, { config, state, driver }) => {
// Custom logic on backdrop click
console.log('Backdrop clicked on step', state.activeIndex);
}element as a functionUseful when the target element is rendered conditionally or changes between renders:
{
element: () => document.querySelector('.dynamic-class'),
popover: { title: 'Dynamic', description: 'Element resolved at render time.' }
}If the highlighted element moves or resizes (e.g., after an accordion opens), call:
driverObj.refresh();driverObj.setConfig({ animate: false });
driverObj.setSteps([
{ element: '#new-step', popover: { title: 'New', description: 'Replaced steps.' } }
]);Driver.js is vanilla TypeScript with zero dependencies. It works in any framework.
Trigger tours inside useEffect or event handlers. Make sure the target elements are mounted before calling drive():
import { useEffect } from 'react';
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
function App() {
useEffect(() => {
const driverObj = driver({
steps: [
{ element: '#step1', popover: { title: 'Welcome', description: 'Start here.' } },
{ element: '#step2', popover: { title: 'Next', description: 'Then here.' } },
]
});
driverObj.drive();
return () => driverObj.destroy();
}, []);
return (
<div>
<div id="step1">First</div>
<div id="step2">Second</div>
</div>
);
}Use onMounted lifecycle hook:
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
let driverObj;
onMounted(() => {
driverObj = driver({ steps: [/* ... */] });
driverObj.drive();
});
onUnmounted(() => driverObj?.destroy());
</script>Always call driverObj.destroy() on component unmount to remove the overlay and event listeners.
driverObj.moveNext() or driverObj.movePrevious() inside your handler. The override disables default navigation entirely.driverObj.destroy() yourself inside the handler.popoverClass, also set the matching border-*-color on the arrow side classes (see Theming section).element if the DOM is dynamic.driverObj.refresh() to recalculate.driver.js/dist/driver.css (or include the CDN link). Without it, the overlay and popover will not render correctly.destroy() on the previous instance before creating a new one.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.