Elegantly Implementing New User Onboarding in React: HagiCode's driver.js Practice
When users open your product for the first time, do they really know where to start? This article discusses our experience with driver.js for new user onboarding in the HagiCode project—just sharing some thoughts to spark discussion.
Background
Have you encountered this scenario: a new user signs up for your product, opens the page, and looks around confused—unsure where to click or what to do. As developers, we assume users will "explore on their own"—after all, human curiosity is limitless. But the reality is—most users will quietly leave within minutes because they can't find the entry point. The story begins abruptly, and ends just as naturally.
New user onboarding is an important solution to this problem, though implementation isn't simple. A good onboarding system needs to:
- Accurately target and highlight page elements
- Support multi-step onboarding flows
- Remember user choices (complete/skip)
- Not affect page performance and normal interactions
- Have clear code structure for easy maintenance
During HagiCode's development, we faced the same challenges. HagiCode is an AI coding assistant project with a core workflow of "user creates proposal → AI generates plan → user reviews → AI executes"—an OpenSpec process. For users new to this concept, this workflow is entirely new, so we needed good onboarding to help them get started quickly. After all, new things always take some time to get used to.
About HagiCode
The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is a Claude-based AI coding assistant that helps developers complete code tasks more efficiently through the OpenSpec workflow. You can view our open-source code on GitHub.
Why Choose driver.js
During the technology selection phase, we evaluated several mainstream onboarding libraries—each has its own characteristics:
- Intro.js: Powerful but large in size, with relatively complex style customization
- Shepherd.js: Well-designed API, but a bit "heavy" for our scenario
- driver.js: Lightweight, concise, intuitive API, and supports React ecosystem
We ultimately chose driver.js, mainly based on these considerations:
- Lightweight: Small core library that won't significantly increase bundle size
- Simple API: Clear and intuitive configuration, quick to get started
- Flexibility: Supports custom positioning, styles, and interaction behaviors
- Dynamic Import: Can be loaded on-demand without affecting first-screen performance
When it comes to technology selection, there's no "best"—only "most suitable."
Technical Implementation
Core Configuration
driver.js configuration is very straightforward. Here's the core configuration from the HagiCode project:
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
const newConversationDriver = driver({
allowClose: true, // Allow users to close the onboarding
animate: true, // Enable animation effects
overlayClickBehavior: 'close', // Click overlay to close onboarding
disableActiveInteraction: false, // Keep elements interactive
showProgress: false, // Don't show progress bar (we have custom progress management)
steps: guideSteps // Array of onboarding steps
});
The considerations behind these configurations:
-
allowClose: true- Respect user choice, don't force completion of onboarding -
disableActiveInteraction: false- Some steps require actual user interaction (like typing), so we can't disable interaction -
overlayClickBehavior: 'close'- Give users a quick way to exit
State Management
Persisting onboarding state is crucial—we don't want to re-onboard users every time they refresh the page. HagiCode uses localStorage to manage onboarding state:
export type GuideState = 'pending' | 'dismissed' | 'completed';
export interface UserGuideState {
session: GuideState;
detailGuides: Record<string, GuideState>;
}
// Read state
export const getUserGuideState = (): UserGuideState => {
const state = localStorage.getItem('userGuideState');
return state ? JSON.parse(state) : { session: 'pending', detailGuides: {} };
};
// Update state
export const setUserGuideState = (state: UserGuideState) => {
localStorage.setItem('userGuideState', JSON.stringify(state));
};
We defined three states:
-
pending: Onboarding in progress, user hasn't completed or skipped yet -
dismissed: User actively closed the onboarding -
completed: User completed all steps
For proposal detail page onboarding, we also support more fine-grained state tracking (through the detailGuides dictionary), because a proposal may go through multiple stages (draft, review, execution complete), each requiring different onboarding. After all, the state of things is always changing.
Target Element Positioning
driver.js uses CSS selectors to locate target elements. HagiCode adopts a convention: using the data-guide custom attribute to mark onboarding targets:
const steps = [
{
element: '[data-guide="launch"]',
popover: {
title: 'Start New Conversation',
description: 'Click here to create a new conversation session...'
}
}
];
Usage in components:
<button data-guide="launch" onClick={handleLaunch}>
New Conversation
</button>
Benefits of this approach:
- Avoids conflicts with business style class names
- Clear semantics—immediately apparent that this element relates to onboarding
- Easy to manage and maintain uniformly
Dynamic Import Optimization
Because onboarding functionality is only needed in specific scenarios (like new users visiting for the first time), we use dynamic import to optimize initial load performance:
const initNewUserGuide = async () => {
// Dynamically import driver.js
const { driver } = await import('driver.js');
await import('driver.js/dist/driver.css');
// Initialize onboarding
const newConversationDriver = driver({
// ...configuration
});
newConversationDriver.drive();
};
This way, driver.js and its styles are only loaded when needed, without affecting first-screen performance. After all, who wants to pay the waiting cost for something temporarily unused?
Onboarding Flow Design
HagiCode implements two onboarding paths covering users' core usage scenarios.
Conversation Onboarding (10 Steps)
This onboarding helps users complete the entire flow from creating a conversation to submitting their first complete proposal:
- launch - Start onboarding, introduce the "New Conversation" button
- compose - Guide user to type request in the input box
- send - Guide clicking the send button
- proposal-launch-readme - Guide creating a README proposal
- proposal-compose-readme - Guide editing README request content
- proposal-submit-readme - Guide submitting README proposal
- proposal-launch-agents - Guide creating an AGENTS.md proposal
- proposal-compose-agents - Guide editing AGENTS.md request
- proposal-submit-agents - Guide submitting AGENTS.md proposal
- proposal-wait - Explain that AI is processing, please wait
The design philosophy behind this onboarding: through two actual proposal creation tasks (README and AGENTS.md), let users personally experience HagiCode's core workflow. After all, knowledge from books is shallow—true understanding comes from practice.
The following images correspond to key nodes in the conversation onboarding:
The first step of conversation onboarding first brings users to the "New Normal Conversation" entry point.
Then guide users to write their first request in the input box, lowering the barrier for that first interaction.
After input is complete, clearly prompt users to send their first message, making the operation path more coherent.
When both proposals are created, the onboarding returns to the conversation list, letting users know they only need to wait for the system to continue executing and refresh.
Proposal Detail Onboarding (3 Steps)
When users enter the proposal detail page, corresponding onboarding is triggered based on the proposal's current state:
- drafting (Draft stage) - Guide users to view AI-generated plan
- reviewing (Review stage) - Guide users to execute the plan
- executionCompleted (Complete stage) - Guide users to archive the plan
This onboarding's characteristic is state-driven—dynamically deciding which onboarding step to display based on the proposal's actual state. Things are always changing, so onboarding should change accordingly.
The image below shows the proposal detail page's onboarding state during the "drafting stage":
At this stage, the onboarding focuses user attention on the key action of "Generate Plan," avoiding confusion about what to do first when entering the detail page for the first time.
Element Rendering Retry Mechanism
In React applications, onboarding target elements may not have finished rendering yet (for example, waiting for asynchronous data loading). To handle this situation, HagiCode implements a retry mechanism:
const waitForElement = (selector: string, maxRetries = 10, interval = 100) => {
let retries = 0;
return new Promise<HTMLElement>((resolve, reject) => {
const checkElement = () => {
const element = document.querySelector(selector) as HTMLElement;
if (element) {
resolve(element);
} else if (retries < maxRetries) {
retries++;
setTimeout(checkElement, interval);
} else {
reject(new Error(`Element not found: ${selector}`));
}
};
checkElement();
});
};
Call this function before initializing onboarding to ensure target elements exist. Sometimes, waiting a bit longer is worth it.
Best Practices Summary
Based on HagiCode's practical experience, here are several key best practices:
1. Onboarding Should Be "Escapable"
Don't force users to complete onboarding. Some users are explorers who prefer to figure things out themselves. Provide a clear "Skip" button and remember their choice so you don't bother them next time. After all, beautiful things or people don't need to be possessed—appreciating their beauty from afar is enough.
2. Onboarding Content Should Be Concise and Powerful
Each onboarding step should focus on a single goal:
- Title: Short and clear, no more than 10 characters
- Description: Get straight to the point, tell users "what this is" and "why use it"
Avoid lengthy explanations—user attention during onboarding is very limited. Say too much, and no one will want to read it.
3. Selectors Should Be Stable
Use stable element marking methods that don't change frequently. The data-guide custom attribute is a good choice—avoid relying on class names or DOM structure, as these easily change during refactoring. Code is always changing, but some things should remain as stable as possible.
4. Test Your Onboarding
HagiCode wrote complete test cases for the onboarding functionality:
describe('NewUserConversationGuide', () => {
it('should correctly initialize onboarding state', () => {
const state = getUserGuideState();
expect(state.session).toBe('pending');
});
it('should correctly update onboarding state', () => {
setUserGuideState({ session: 'completed', detailGuides: {} });
const state = getUserGuideState();
expect(state.session).toBe('completed');
});
});
Testing ensures that when refactoring code, you won't accidentally break onboarding functionality. After all, no one wants to break existing functionality while making changes.
5. Performance Optimization
- Use dynamic imports to lazy load the onboarding library
- Avoid initializing onboarding logic after users have completed it
- Consider the performance impact of onboarding animations—can disable animations on low-end devices
Performance, like life, should be conserved where it should be.
Summary
New user onboarding is an important part of improving product user experience. In the HagiCode project, we used driver.js to build a complete onboarding system covering the entire workflow from conversation creation to proposal execution.
Through this article, we hope to convey these core points:
- Technology selection should match requirements: driver.js isn't the most powerful, but it's the most suitable for us
- State management is crucial: Use localStorage to persist onboarding state and avoid repeatedly bothering users
- Onboarding design should be focused: Each step solves one problem, don't try to do too much
- Code structure should be clear: Separate onboarding configuration, state management, and UI logic for easy maintenance
If you're adding new user onboarding functionality to your project, I hope the practical experience shared in this article helps you. Actually, technology isn't that mysterious—try more, summarize more, and it'll get better over time...
References
- driver.js Official Documentation
- HagiCode Project Source Code
- HagiCode Official Website
- OpenSpec Workflow Documentation
Original Article & License
Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.
- Author: newbe36524
- Original URL: https://docs.hagicode.com/go?platform=devto&target=%2Fblog%2F2026-04-01-new-user-guide-with-driverjs%2F
- License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.





Top comments (0)