Why Onboarding Is Usually Deprioritised

In every product roadmap I have been part of, user onboarding gets the same treatment: it is acknowledged as important, it gets slotted into "phase two," and phase two never arrives. The technical reason is usually that adding a product tour to a Laravel Blade application feels disproportionately complicated relative to the feature's apparent simplicity.

Existing JavaScript-based tour libraries like Intro.js, Shepherd.js, and Driver.js all require:

  1. Adding the library to your JavaScript bundle (or loading via CDN)
  2. Writing tour step definitions in JavaScript — often in separate .js files disconnected from your PHP business logic
  3. Building a system to track which users have seen which tours
  4. Maintaining synchronisation between PHP-side user state and JavaScript-side tour state

Laravel Driver.js eliminates all four of these complexities. You define your entire tour in PHP, the package handles the JavaScript configuration generation, and tour completion tracking hooks into Laravel's session and database systems natively.

The Package Architecture

The core of the package is a TourBuilder class that stores step definitions and generates the Driver.js configuration JSON that the frontend component needs.

namespace AmjadIqbal\LaravelDriverJs;

class TourBuilder
{
    protected string $id;
    protected array $steps = [];
    protected array $config = [];
    protected bool $showOnce = false;

    public function step(
        string $element,
        string $title,
        string $description,
        string $side = 'bottom',
        string $align = 'start'
    ): static {
        $this->steps[] = [
            'element' => $element,
            'popover' => [
                'title' => $title,
                'description' => $description,
                'side' => $side,
                'align' => $align,
            ],
        ];
        return $this;
    }

    public function showOnce(): static
    {
        $this->showOnce = true;
        return $this;
    }

    public function toJson(): string
    {
        return json_encode([
            'id' => $this->id,
            'showOnce' => $this->showOnce,
            'config' => $this->config,
            'steps' => $this->steps,
        ]);
    }
}

The TourRegistry singleton collects all registered tours at boot time. The @driverjs Blade directive iterates over registered tours and outputs the necessary JavaScript initialisation:

<!-- Output by @driverjs directive -->
<script>
window.laravelDriverJsTours = {!! $tours->toJson() !!};
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
    window.laravelDriverJsTours.forEach(function(tour) {
        if (tour.showOnce && localStorage.getItem('tour_' + tour.id)) return;

        const driver = window.driver.js.driver({
            ...tour.config,
            steps: tour.steps,
            onDestroyed: function() {
                localStorage.setItem('tour_' + tour.id, '1');
            }
        });

        driver.drive();
    });
});
</script>

Registering Tours in a Service Provider

The recommended pattern is to register tours in a service provider or in the controller that serves the page the tour targets:

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        DriverJs::tour('dashboard-welcome')
            ->step('#create-project-btn', 'Create Your First Project',
                   'Click here to start building. You can create as many projects as you need.')
            ->step('#nav-settings', 'Configure Settings',
                   'Update your profile, notification preferences, and billing here.')
            ->step('#help-link', 'Need Help?',
                   'Access documentation and support from this menu at any time.')
            ->showOnce()
            ->register();
    }
}

Conditional Tour Display

A common requirement is showing different tours to different user types, or only triggering a tour when a user is new. The package supports this through conditional registration:

if (auth()->check() && auth()->user()->created_at->isAfter(now()->subDay())) {
    DriverJs::tour('new-user-welcome')
        ->step('#header', 'Welcome!', 'Let us show you around.')
        ->register();
}

Database-Backed Tour Completion

For cases where localStorage is insufficient (multi-device tracking, server-side tour gating), the package provides a database-backed completion tracker:

// In your migration
Schema::create('tour_completions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('tour_id');
    $table->timestamp('completed_at');
});
// Usage
DriverJs::tour('new-feature-2026')
    ->trackInDatabase()
    ->showOnce()
    ->step('...')
    ->register();

When trackInDatabase() is enabled, the tour completion event sends a fetch request to a package-provided API endpoint, and subsequent page loads check the database before rendering the tour.

Real-World Use Cases

During development, I tested the package against three real scenarios:

New user onboarding — a five-step tour triggered for users in their first 24 hours. Reduced support questions about basic features by roughly 40% in user testing.

Feature announcement — a three-step tour triggered once for all existing users after a major UI redesign. Effectively replaced the "what's new" modal that typically gets ignored.

Role-specific guidance — different tours for admin users vs. standard users, each highlighting the controls and menus relevant to their access level.

GitHub Repository

The full source, API reference, and database integration documentation are available on GitHub.

composer require amjadiqbal/laravel-driver-js

Conclusion

User onboarding does not have to be technically complicated. With Laravel Driver.js, you can define an entire multi-step product tour in pure PHP, track completion per user or per device, and deploy it without touching a single JavaScript file. If "we should add onboarding" has been on your backlog for a while, this package removes the technical reason to keep deferring it.