Skip to content

new user onboarding

1 post with the tag “new user onboarding”

Building Elegant New User Onboarding in React Projects: HagiCode's driver.js Practice

Building Elegant New User Onboarding in React Projects: HagiCode’s driver.js Practice

Section titled “Building Elegant New User Onboarding in React Projects: HagiCode’s driver.js Practice”

When users open your product for the first time, do they really know where to start? In this article, I want to talk a bit about how we used driver.js for new user onboarding in the HagiCode project. Consider it a small practical example to get the conversation started.

Have you ever run into this situation? A new user signs up for your product, opens the page, and immediately looks lost. They scan around, unsure what to click or what to do next. As developers, we often assume users will just “explore on their own” because, after all, human curiosity is limitless. But reality is different: most users quietly leave within minutes if they cannot find the right entry point, as if the story begins suddenly and ends just as naturally.

New user onboarding is an important way to solve this problem, but building it well is not that simple. A good onboarding system needs to:

  • Precisely locate page elements and highlight them
  • Support multi-step onboarding flows
  • Remember the user’s choice (complete or skip)
  • Avoid affecting page performance and normal interaction
  • Keep the code structure clear and easy to maintain

While building HagiCode, we ran into the same challenge. HagiCode is an AI coding assistant, and its core workflow is an OpenSpec workflow that looks like this: “the user creates a Proposal -> the AI generates a plan -> the user reviews it -> the AI executes it.” For users encountering this concept for the first time, the workflow is completely new, so they need solid onboarding to get started quickly. New things always take a little time to get used to.

The approach shared in this article comes from our hands-on experience in the HagiCode project. HagiCode is a Claude-based AI coding assistant that helps developers complete coding tasks more efficiently through the OpenSpec workflow. You can view our open-source code on GitHub.

During the technical evaluation phase, we looked at several mainstream onboarding libraries. Each one had its own strengths:

  • Intro.js: Powerful, but relatively large, and style customization is somewhat complex
  • Shepherd.js: Well-designed API, but a bit too “heavy” for our use case
  • driver.js: Lightweight, concise, intuitive API, and works well in the React ecosystem

In the end, we chose driver.js. There was no especially dramatic reason. The choice mainly came down to these considerations:

  1. Lightweight: The core library is small and does not significantly increase bundle size
  2. Simple API: The configuration is clear and intuitive, so it is easy to pick up
  3. Flexible: Supports custom positioning, styling, and interaction behavior
  4. Dynamic import: Can be loaded on demand without affecting first-screen performance

With technology selection, there is rarely a universally best answer. Usually, there is only the option that fits best.

driver.js has a very intuitive configuration model. Here is the core configuration we use in the HagiCode project:

import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
const newConversationDriver = driver({
allowClose: true, // Allow users to close the guide
animate: true, // Enable animations
overlayClickBehavior: 'close', // Close the guide when the overlay is clicked
disableActiveInteraction: false, // Keep elements interactive
showProgress: false, // Do not show the progress bar (we manage progress ourselves)
steps: guideSteps // Array of guide steps
});

The reasoning behind these settings is:

  • allowClose: true - Respect the user’s choice and do not force them to finish the guide
  • disableActiveInteraction: false - Some steps require real user actions, such as typing input, so interaction cannot be disabled
  • overlayClickBehavior: 'close' - Give users a quick way to exit

Persisting onboarding state is critical. We do not want to restart the guide every time the page refreshes, because that gets annoying fast. HagiCode uses localStorage to manage guide 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 define three states:

  • pending: The guide is still in progress, and the user has not completed or skipped it
  • dismissed: The user closed the guide proactively
  • completed: The user completed all steps

For Proposal detail page onboarding, we also support more fine-grained state tracking through the detailGuides map, because one Proposal can go through multiple stages - draft, review, and execution complete - and each stage needs different guidance. The state of things is always changing, and onboarding should reflect that.

driver.js uses CSS selectors to locate target elements. HagiCode follows a simple convention: use a custom data-guide attribute to mark onboarding targets:

const steps = [
{
element: '[data-guide="launch"]',
popover: {
title: 'Start a New Conversation',
description: 'Click here to create a new conversation session...'
}
}
];

In components, it looks like this:

<button data-guide="launch" onClick={handleLaunch}>
New Conversation
</button>

The benefits of this approach are:

  • Avoid conflicts with business styling class names
  • Clear semantics, so you can immediately tell the element is related to onboarding
  • Easier to manage and maintain consistently

Because the onboarding feature is only needed in specific scenarios, such as a user’s first visit, we use dynamic imports to optimize initial loading performance:

const initNewUserGuide = async () => {
// Dynamically import driver.js
const { driver } = await import('driver.js');
await import('driver.js/dist/driver.css');
// Initialize the guide
const newConversationDriver = driver({
// ...configuration
});
newConversationDriver.drive();
};

This way, driver.js and its stylesheet are only loaded when needed and do not affect first-screen performance. Not many people enjoy waiting for something they do not even need yet.

HagiCode implements two onboarding paths that cover the user’s core scenarios.

This onboarding path helps users complete the entire flow from creating a conversation to submitting their first complete Proposal:

  1. launch - Start the guide and introduce the “New Conversation” button
  2. compose - Guide the user to type a request in the input box
  3. send - Guide the user to click the send button
  4. proposal-launch-readme - Guide the user to create a README Proposal
  5. proposal-compose-readme - Guide the user to edit the README request content
  6. proposal-submit-readme - Guide the user to submit the README Proposal
  7. proposal-launch-agents - Guide the user to create an AGENTS.md Proposal
  8. proposal-compose-agents - Guide the user to edit the AGENTS.md request
  9. proposal-submit-agents - Guide the user to submit the AGENTS.md Proposal
  10. proposal-wait - Explain that the AI is processing and the user should wait a moment

The idea behind this path is to let users experience HagiCode’s core workflow firsthand through two real Proposal creation tasks, one for README and one for AGENTS.md. There is a big difference between hearing about a workflow and going through it yourself.

The following screenshots correspond to a few key points in the session onboarding flow:

Session onboarding: starting from creating a conversation session

The first step of session onboarding takes the user to the entry point for creating a new Conversation session.

Session onboarding: entering the first request

Next, the guide prompts the user to type their first request into the input box, lowering the barrier to getting started.

Session onboarding: sending the first message

After the input is complete, the guide clearly prompts the user to send the first message so the action flow feels more connected.

Session onboarding: waiting in the session list for continued execution

Once both Proposals have been created, the guide returns to the session list and lets the user know that the next step is simply to wait for the system to continue execution and refresh.

When users enter the Proposal detail page, HagiCode triggers the corresponding guide based on the Proposal’s current state:

  1. drafting (draft stage) - Guide the user to review the AI-generated plan
  2. reviewing (review stage) - Guide the user to execute the plan
  3. executionCompleted (completed stage) - Guide the user to archive the plan

The defining characteristic of this guide is that it is state-driven: it dynamically decides which onboarding step to show based on the Proposal’s actual state. Things change, and onboarding should change with them.

The screenshot below shows the Proposal detail page in its onboarding state during the drafting phase:

Proposal detail onboarding: generate the plan first during drafting

At this stage, the guide focuses the user’s attention on the key action of generating a plan, so they do not wonder what to do first when entering the detail page for the first time.

In React applications, the target onboarding element may not have finished rendering yet, for example because asynchronous data is still loading. To handle this, 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 the guide to make sure the target element already exists. Sometimes waiting a little longer is worth it.

Based on HagiCode’s practical experience, here are a few key best practices:

Do not force users to complete onboarding. Some users are explorers by nature and prefer to figure things out on their own. Provide a clear “Skip” button, remember their choice, and do not interrupt them again next time.

2. Keep Onboarding Content Short and Sharp

Section titled “2. Keep Onboarding Content Short and Sharp”

Each onboarding step should focus on a single goal:

  • Title: Short and clear, ideally no more than a few words
  • Description: Get straight to the point and tell the user what this is and why it matters

Avoid long-winded explanations. User attention is limited during onboarding, and the more you say, the less likely they are to read it.

Use stable element markers that do not change often. The custom data-guide attribute is a good choice. Avoid depending on class names or DOM structure, because those are easy to change during refactors. Code changes all the time, but some anchors should stay stable when possible.

HagiCode includes complete test cases for the onboarding feature:

describe('NewUserConversationGuide', () => {
it('should initialize guide state correctly', () => {
const state = getUserGuideState();
expect(state.session).toBe('pending');
});
it('should update guide state correctly', () => {
setUserGuideState({ session: 'completed', detailGuides: {} });
const state = getUserGuideState();
expect(state.session).toBe('completed');
});
});

Tests help ensure that refactoring does not accidentally break onboarding behavior. Nobody wants a small code change to quietly damage previously working functionality.

  • Use dynamic imports to lazy-load the onboarding library
  • Avoid initializing onboarding logic after the user has already completed the guide
  • Consider the performance impact of animations, and disable them on lower-end devices if needed

Performance, like many things in life, deserves a bit of careful budgeting.

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 that covers the full workflow from session creation to Proposal execution.

The core points we hope to share through this article are:

  1. Technical choices should match actual needs: driver.js is not the most powerful option, but it is the best fit for us
  2. State management is critical: Use localStorage to persist onboarding state and avoid repeatedly interrupting users
  3. Onboarding design should stay focused: Each step should solve one problem, and no more
  4. Code structure should stay clear: Separate onboarding configuration, state management, and UI logic to make maintenance easier

If you are adding new user onboarding to your own project, I hope the practical experience in this article helps. There is nothing especially mystical about this kind of technology. Keep trying, keep summarizing what you learn, and things gradually get easier.

Thank you for reading. If you found this article useful, feel free to like, bookmark, and share it. This content was created with AI-assisted collaboration, and the final content was reviewed and confirmed by the author.