DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

Elegantly Implementing New User Onboarding in React: HagiCode's driver.js Practice

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:

  1. Lightweight: Small core library that won't significantly increase bundle size
  2. Simple API: Clear and intuitive configuration, quick to get started
  3. Flexibility: Supports custom positioning, styles, and interaction behaviors
  4. 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
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

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...'
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

Usage in components:

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

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();
};
Enter fullscreen mode Exit fullscreen mode

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:

  1. launch - Start onboarding, introduce the "New Conversation" button
  2. compose - Guide user to type request in the input box
  3. send - Guide clicking the send button
  4. proposal-launch-readme - Guide creating a README proposal
  5. proposal-compose-readme - Guide editing README request content
  6. proposal-submit-readme - Guide submitting README proposal
  7. proposal-launch-agents - Guide creating an AGENTS.md proposal
  8. proposal-compose-agents - Guide editing AGENTS.md request
  9. proposal-submit-agents - Guide submitting AGENTS.md proposal
  10. 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:

Conversation onboarding: Starting from creating a normal conversation

The first step of conversation onboarding first brings users to the "New Normal Conversation" entry point.

Conversation onboarding: Type first request

Then guide users to write their first request in the input box, lowering the barrier for that first interaction.

Conversation onboarding: Send first message

After input is complete, clearly prompt users to send their first message, making the operation path more coherent.

Conversation onboarding: Wait for conversation list to continue execution

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:

  1. drafting (Draft stage) - Guide users to view AI-generated plan
  2. reviewing (Review stage) - Guide users to execute the plan
  3. 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":

Proposal detail onboarding: Generate plan first in 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();
  });
};
Enter fullscreen mode Exit fullscreen mode

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Technology selection should match requirements: driver.js isn't the most powerful, but it's the most suitable for us
  2. State management is crucial: Use localStorage to persist onboarding state and avoid repeatedly bothering users
  3. Onboarding design should be focused: Each step solves one problem, don't try to do too much
  4. 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

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.

Top comments (0)