# laravel-onboard turns onboarding steps into a chain of closures

> Spatie's laravel-onboard lets you declare onboarding steps in a service provider, decide each step's completion with a callback against your model, and render the list yourself, with no views and no tables shipped. It suits Laravel applications that want a progress checklist without building a checklist feature from scratch.

**spatie/laravel-onboard** — A Laravel package to help track user onboarding steps

- Repository: https://github.com/spatie/laravel-onboard
- Website: https://rias.be/blog/laravel-package-to-onboard-your-users
- Stars: 847 · Forks: 31
- Language: PHP
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-onboard

## Steps defined in code rather than in a table

The whole package fits in a small idea. You declare onboarding steps with a fluent builder, each one carrying a title, a link, a call to action and a condition, and the package then exposes that list to your application. The README shows the shape of the definition:

```php
use App\User;
use Spatie\Onboard\Facades\Onboard;

Onboard::addStep('Complete Profile')
    ->link('/profile')
    ->cta('Complete')
    ->completeIf(function (User $model) {
        return $model->profile->isComplete();
    });
```

The steps are registered through the `Onboard` facade, and the README places that registration in `App\Providers\AppServiceProvider.php`. The important consequence is that the flow is code. There is no admin screen, no seed data and no migration in the repository, and the top level entries are the usual package files such as `src/`, `tests/`, `composer.json` and `phpunit.xml.dist`.

## Completion is a closure evaluated against your model

Each step decides its own state through `completeIf`, a callback that receives the object you attached the package to. The README notes that the callback receives the class that you added the interface and trait to, and that you can use Laravel's dependency injection there to inject anything else as well, so a step can consult another service. There is one sharp edge. The variable name passed to the `completeIf` callback must be `$model`, which the README states explicitly, so the injected type hint and the parameter name have to agree. That constraint is small but it is the kind of detail that produces a confusing failure if you rename the argument to something more descriptive.

## A trait and an interface make a class onboardable

The wiring into your model is two lines of PHP. You add the `Spatie\Onboard\Concerns\GetsOnboarded` trait and the `Spatie\Onboard\Concerns\Onboardable` interface to any model or class in your app, and the README's example is the `User` model:

```php
class User extends Model implements \Spatie\Onboard\Concerns\Onboardable
{
    use \Spatie\Onboard\Concerns\GetsOnboarded;
    ...
```

The trait is what gives the object its `onboarding()` method, and the interface is the contract the package checks when it dispatches the callback. The design is deliberately thin. The package does not define a step model, does not create tables and does not own the rendering, so the only thing it injects into your class is the ability to ask what the onboarding state is right now.

## The package renders nothing, you render everything

The README is explicit that you can render the onboarding flow however you want in your templates, and it gives a full Blade example that loops over the steps, marks a completed one with a check icon and a struck through title, and disables the link when the step is done:

```blade
@if (auth()->user()->onboarding()->inProgress())
    <div>
        @foreach (auth()->user()->onboarding()->steps as $step)
            <span>
                @if($step->complete())
                    <i class="fa fa-check-square-o fa-fw"></i>
                    <s>{{ $loop->iteration }}. {{ $step->title }}</s>
                @else
                    <i class="fa fa-square-o fa-fw"></i>
                    {{ $loop->iteration }}. {{ $step->title }}
                @endif
            </span>
```

That example is a checklist, a progress bar or a modal are all equally valid, and the absence of any view or theme in the package is the design rather than an omission. It also means the styling, the markup and the accessibility of your onboarding surface are entirely your problem.

## Progress, completion and per-step state

The manager object exposes the state you need for a header badge, a progress bar or a redirect. The README lists the available calls, and they are few on purpose:

```php
$onboarding = Auth::user()->onboarding();

$onboarding->inProgress();

$onboarding->percentageCompleted();

$onboarding->finished();

$onboarding->steps()->each(function($step) {
    $step->title;
    $step->cta;
    $step->link;
    $step->complete();
    $step->incomplete();
});
```

Note the direction of the dependency. Nothing is stored; the state is computed each time you ask, by running the callbacks against the model. That is elegant when your steps are derivable from data you already store, such as a completed profile or a first post, and it is the constraint to think about when they are not.

## Excluding steps and limiting them to a class

Two mechanisms handle the cases where the same list does not apply to everyone. A step can be excluded conditionally with `excludeIf`, which the README shows returning true for administrators, so admins never see that step at all. A step can also be limited to a specific class, either by passing the class itself or its string name:

```php
Onboard::addStep('Excluded Step')
    ->excludeIf(function (User $model) {
        return $model->isAdmin();
    });
```

The README spells out the resolution rules for limited steps, which are worth reading before you rely on them. Steps that are not limited are available to all classes, so with one User step, one Team step and one normal step, other classes see only the normal step, a User sees the normal step and the User step, and a Team sees the normal step and the Team step. Since the same registration code serves every class, a step that applies to a type of object that does not exist in your application is simply inert.

## Custom attributes for arbitrary per-step data

Steps are not limited to a title, a link and a call to action. You can attach your own attributes to a step and read them back later, which is how you would carry a description, an icon name or a tooltip without a parallel data structure:

```php
// Defining the attributes
Onboard::addStep('Step w/ custom attributes')
    ->attributes([
        'name' => 'Waldo',
        'shirt_color' => 'Red & White',
    ]);

// Accessing them
$step->name;
$step->shirt_color;
```

The access is by property, so a typo in an attribute name shows up as a missing value rather than an error, which is a small quality of life consideration when you have many steps defined across a service provider.

## Redirecting to the next unfinished step, and where the project stands

The README closes the usage guide with a middleware example whose purpose is to ensure a user is redirected to the next unfinished onboarding step whenever they access the web application, described as a starting point rather than a finished component. The class name in the example is `RedirectToUnfinishedOnboardingStep` in the `App\Http\Middleware` namespace, and it checks `auth()->user()->onboarding()->inProgress()` before redirecting to the next step's link. On the project itself, the licence is MIT, the package is published on Packagist, and the repository carries an `UPGRADING.md` next to the `CHANGELOG.md` and a `LICENSE.md`, which is the usual sign of a package with breaking changes between majors. Releases have been steady: 2.6.3 on 2026-02-26, 2.6.2 on 2025-03-21 and 2.6.1 on 2025-02-14, and the last push was on 2026-06-26.

## Conclusion

Adopt laravel-onboard when your onboarding steps are all derivable from state on your own models, since a step's completion is a closure you write and the package ships no interface of its own to fight with. Do not adopt it as a general task or checklist system, because the README documents no storage of progress and no views, so anything not derivable from a model has nowhere to live. Verify first that the variable in the `completeIf` callback is named `$model`, as the README requires, and note that the package is MIT licensed with the last push on 2026-06-26 and version 2.6.3 published on 2026-02-26.

## FAQ

### How do I install spatie/laravel-onboard?

Through Composer, with `composer require spatie/laravel-onboard`. Then add the `GetsOnboarded` trait and the `Onboardable` interface to the model or class you want to onboard, and register your steps in a service provider.

### How does a step know when it is complete?

Each step takes a `completeIf` callback that receives your model, and Laravel's dependency injection can be used inside it to inject other services. The variable name passed to the callback must be `$model`.

### Does laravel-onboard ship views or a database migration?

No. The README says you can render the onboarding flow however you want in your templates, and the repository lists no migration or table, so both the markup and any state you want to keep are yours to build.

### Can I show different onboarding steps to different models?

Yes. A step can be limited to a class by passing the class or its name, and steps that are not limited are available to all classes. A step can also be skipped with `excludeIf`, for example when the model is an administrator.

## Sources

- [License: MIT](https://github.com/spatie/laravel-onboard/blob/main/LICENSE)
- [Project website](https://rias.be/blog/laravel-package-to-onboard-your-users)
- [README](https://github.com/spatie/laravel-onboard/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-onboard/releases)
- [spatie/laravel-onboard on GitHub](https://github.com/spatie/laravel-onboard)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-onboard
