coringawc / filament-tutorials
Filament plugin for interactive panel tutorials powered by Driver.js and stable targets.
Requires
- php: ^8.4
- filament/filament: ^5.0
- spatie/laravel-package-tools: ^1.15
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/boost: ^2.4
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.9
- orchestra/testbench: ^11.0
- orchestra/workbench: ^11.0
- pestphp/pest: ^4.6
- pestphp/pest-plugin-browser: ^4.3
- pestphp/pest-plugin-laravel: ^4.1
- phpunit/phpunit: ^12.0
- rector/rector: ^2.0
README
Interactive tutorials for Filament v5 panels, powered by Driver.js and stable, Filament-aware targets.
The package registers its assets only on panels where the plugin is enabled, discovers tutorial classes by panel, renders the tutorial launcher only on pages that have a guide, and can persist first-run progress per authenticated user.
Installation
Install the package and publish its configuration and migration:
composer require coringawc/filament-tutorials php artisan vendor:publish --tag=filament-tutorials-config php artisan vendor:publish --tag=filament-tutorials-migrations php artisan migrate php artisan filament:assets
The migration is required while filament-tutorials.progress.enabled is
true. Progress persistence is enabled by default.
Quick Start
1. Register the plugin in the panel
Open the panel provider where the tutorial should be available, such as
app/Providers/Filament/AppPanelProvider.php, and register the plugin in the
panel() method:
use CoringaWc\FilamentTutorials\FilamentTutorialsPlugin; use Filament\Panel; public function panel(Panel $panel): Panel { return $panel ->id('app') ->plugin( FilamentTutorialsPlugin::make() ); }
The value passed to Panel::id() determines the default tutorial directory and
namespace. It is not derived from the provider class name.
| Panel id | Default directory | Default namespace |
|---|---|---|
app |
app/Filament/App/Tutorials |
App\Filament\App\Tutorials |
admin |
app/Filament/Admin/Tutorials |
App\Filament\Admin\Tutorials |
For another panel id, convert it to StudlyCase. A panel with id
agency-portal, for example, discovers classes from
app/Filament/AgencyPortal/Tutorials under the
App\Filament\AgencyPortal\Tutorials namespace.
2. Create the tutorial class
The package does not currently provide a dedicated
make:filament-tutorial Artisan command. Create the class manually, or use
Laravel's generic make:class command:
php artisan make:class Filament/App/Tutorials/ContractingListTutorial --no-interaction
For the app panel, the resulting file must be:
app/Filament/App/Tutorials/ContractingListTutorial.php
Replace the generated class with a tutorial that extends FilamentTutorial:
<?php declare(strict_types=1); namespace App\Filament\App\Tutorials; use App\Filament\App\Resources\ContractingDraftResource; use App\Filament\App\Resources\ContractingDraftResource\Pages\ListContractingDrafts; use CoringaWc\FilamentTutorials\FilamentTutorial; use CoringaWc\FilamentTutorials\TutorialStep; final class ContractingListTutorial extends FilamentTutorial { protected static ?string $page = ListContractingDrafts::class; /** * @param list<TutorialStep>|null $steps * @return ($steps is null ? list<TutorialStep> : static) */ public function steps(?array $steps = null): array|static { if ($steps !== null) { return parent::steps($steps); } return [ TutorialStep::make('contracting-drafts.page-overview') ->targetPage(ListContractingDrafts::class) ->title(__('Contractings')) ->description(__('Review the main information available on this page.')), TutorialStep::make('contracting-drafts.navigation-item') ->targetNavigation(ContractingDraftResource::class) ->beforeOpenSidebar() ->title(__('Contractings menu')) ->description(__('Use this menu to return to your contractings.')), ]; } }
No additional registration is necessary for this class. Because the file and
namespace follow the convention for the app panel, the plugin discovers it
automatically.
3. Open the page
Open the page represented by ListContractingDrafts::class. The question-mark
launcher appears beside the Filament user menu because that page now has a
tutorial. It is intentionally hidden on pages without a registered tutorial.
If the launcher does not appear, see Troubleshooting.
Understanding Tutorial And Step Keys
The value passed to TutorialStep::make() is the step's own identifier. It is
not a target type, CSS selector, translation key, title, or reserved word.
For example, this is valid:
TutorialStep::make('contracting-draft-page') ->targetPage(ListContractingDrafts::class);
You do not need to write page inside make() when using targetPage().
Names such as page, navigation, action, and field in short examples are
only illustrative keys. The method called after make() determines what the
step highlights.
Prefer descriptive, stable keys:
TutorialStep::make('contracting-drafts.page-overview'); TutorialStep::make('contracting-drafts.navigation-item'); TutorialStep::make('contracting-drafts.create-action'); TutorialStep::make('contracting-drafts.title-field');
There are three separate identifiers to understand:
| Identifier | Example | Purpose |
|---|---|---|
| Tutorial key | contracting-list |
Identifies the complete tutorial and its saved progress. |
| Step key | contracting-drafts.title-field |
Identifies one step inside that tutorial. |
| Target key | contracting.form.title |
Connects a target helper to a rendered data-tour attribute. |
For discovered tutorial classes, the tutorial key is derived from the class
name. ContractingListTutorial becomes contracting-list. You may override it
with ->key('another-stable-key') when registering an object directly.
For inline tutorials, always provide the tutorial key explicitly:
FilamentTutorial::make('contracting-drafts.list');
Keys used for persisted progress must:
- start with a lowercase letter or number;
- contain only lowercase letters, numbers,
.,_, and-; - remain stable after release;
- be unique within their scope. Step keys must be unique inside their tutorial.
Use keys as technical identifiers. Put user-facing copy in title() and
description() instead of using translated text as a key.
Choosing A Registration Style
Discovered class
Use a discovered class for normal page tutorials and any flow with multiple
steps. Place it in the panel's Tutorials directory as shown in the quick
start. This keeps Page and Resource classes focused on their Filament concerns.
Inline tutorial
For a very small guide, a Filament Page, Resource, or Resource Page can implement
HasFilamentTutorials directly:
use CoringaWc\FilamentTutorials\Contracts\HasFilamentTutorials; use CoringaWc\FilamentTutorials\FilamentTutorial; use CoringaWc\FilamentTutorials\TutorialStep; use Filament\Pages\Page; final class ListContractingDrafts extends Page implements HasFilamentTutorials { public static function tutorials(): FilamentTutorial { return FilamentTutorial::make('contracting-drafts.list') ->steps([ TutorialStep::make('contracting-drafts.page-overview') ->targetPage() ->title(__('Contractings')) ->description(__('Review the main information available on this page.')), ]); } }
When an inline tutorial does not call forPage(), the plugin associates it with
the Page or Resource that returned it.
Explicit panel registration
Explicit registration is useful for tutorials supplied by another package or for a dashboard that does not need its own discovered class:
use CoringaWc\FilamentTutorials\FilamentTutorial; use CoringaWc\FilamentTutorials\FilamentTutorialsPlugin; use CoringaWc\FilamentTutorials\TutorialStep; FilamentTutorialsPlugin::make() ->tutorials( FilamentTutorial::make('contracts-overview') ->forPage(ContractsDashboard::class) ->steps([ TutorialStep::make('contracts-overview.page') ->targetPage(ContractsDashboard::class), ]), );
You may also pass a tutorial class:
FilamentTutorialsPlugin::make() ->tutorials(ContractingListTutorial::class);
Targets
Targets tell Driver.js which rendered element should be highlighted. Use the
package helpers instead of Filament's internal .fi-* CSS classes.
The step key in make() remains a free, descriptive identifier in every
example below:
TutorialStep::make('contracting-drafts.page-overview') ->targetPage(ListContractingDrafts::class); TutorialStep::make('contracting-drafts.navigation-item') ->targetNavigation(ContractingDraftResource::class); TutorialStep::make('contracting-drafts.global-search') ->targetRenderHook('global-search.before'); TutorialStep::make('contracting-drafts.create-action') ->targetAction('create', ListContractingDrafts::class); TutorialStep::make('contracting-drafts.title-field') ->targetComponent('contracting.form.title');
Targets provided automatically
The plugin exposes these targets without modifying the related Page or Resource:
targetPage()targets the current or supplied Filament Page class.targetNavigation()targets the navigation item belonging to a registered Filament Page or Resource.targetRenderHook()targets a key configured infilament-tutorials.render_hook_targets.
The published configuration already includes keys such as
global-search.before, sidebar.nav.start, topbar.start,
user-menu.before, and resource.list.table.before.
To register another render-hook target, map your stable key to a Filament render
hook in config/filament-tutorials.php:
use Filament\View\PanelsRenderHook; 'render_hook_targets' => [ 'global-search.before' => PanelsRenderHook::GLOBAL_SEARCH_BEFORE, 'page.header.actions.after' => PanelsRenderHook::PAGE_HEADER_ACTIONS_AFTER, ],
The string passed to targetRenderHook() must match the configuration key:
TutorialStep::make('contracting-drafts.header-actions') ->targetRenderHook('page.header.actions.after');
Targets that must be exposed on a component
Actions, fields, schema components, widgets, and custom elements need a stable
data-tour attribute on the component that will be highlighted. Generate that
attribute with TutorialTargetAttributes so it always matches the target
helper.
For a Filament Action:
use CoringaWc\FilamentTutorials\TutorialTargetAttributes; use Filament\Actions\CreateAction; CreateAction::make() ->extraAttributes( TutorialTargetAttributes::action('create', ListContractingDrafts::class) );
The matching tutorial step is:
TutorialStep::make('contracting-drafts.create-action') ->targetAction('create', ListContractingDrafts::class);
For a form field:
use CoringaWc\FilamentTutorials\TutorialTargetAttributes; use Filament\Forms\Components\TextInput; TextInput::make('title') ->extraInputAttributes( TutorialTargetAttributes::component('contracting.form.title') );
The matching tutorial step is:
TutorialStep::make('contracting-drafts.title-field') ->targetComponent('contracting.form.title');
TextInput::make('title') defines the Filament field name.
TutorialStep::make('contracting-drafts.title-field') defines the tutorial step
key. targetComponent('contracting.form.title') identifies the rendered target.
These values have different responsibilities and do not need to be equal.
For a completely custom target:
$attributes = TutorialTargetAttributes::custom('contracting.summary'); TutorialStep::make('contracting-drafts.summary') ->target('contracting.summary');
Apply $attributes to the element through the component's supported
extraAttributes() API.
Preparing Hidden Or Dynamic Targets
Lifecycle actions make an element visible before Driver.js tries to highlight it.
Sidebar navigation
Use beforeOpenSidebar() when a navigation item may be hidden by the responsive
sidebar:
TutorialStep::make('contracting-drafts.navigation-item') ->targetNavigation(ContractingDraftResource::class) ->beforeOpenSidebar();
Modal content
Expose both the action that opens the modal and the field that will be
highlighted. Then use the generated action selector in beforeOpenModal():
TutorialStep::make('contracting-drafts.modal-title-field') ->targetComponent('contracting.form.title') ->beforeOpenModal([ 'selector' => TutorialTargetAttributes::selector( TutorialTargetAttributes::action( 'create', ListContractingDrafts::class, ), ), ]);
Dropdown and collapsible content
TutorialStep::make('contracting-drafts.filters-menu') ->target('contracting.filters.menu') ->beforeOpenDropdown('[data-tour="contracting.filters.trigger"]'); TutorialStep::make('contracting-drafts.advanced-panel') ->target('contracting.advanced.panel') ->beforeOpenCollapsible( trigger: '[data-tour="contracting.advanced.trigger"]', panel: '[data-tour="contracting.advanced.panel"]', );
Available lifecycle helpers include:
beforeOpenSidebar()andafterOpenSidebar();beforeOpenProfileMenu();beforeOpenModal();beforeOpenDropdown();beforeOpenCollapsible();beforeClick()andafterClick();beforeWaitFor();afterHide().
Use ->optional() when a valid page state may legitimately omit the target.
Optional steps are skipped instead of stopping the tutorial.
First-Run Tutorials
Use autoStart() when a tutorial should open automatically the first time the
authenticated user reaches the page:
FilamentTutorial::make('first-run-contracting') ->forPage(ListContractingDrafts::class) ->autoStart() ->steps([ TutorialStep::make('first-run-contracting.page-overview') ->targetPage(ListContractingDrafts::class) ->title(__('Contractings')) ->description(__('Review and create contractings from this page.')), ]);
The tutorial stops opening automatically after that user completes or dismisses it. The launcher remains available so the tutorial can be opened again.
When the dismissal reminder is enabled, the first step includes an Ignore action. Ignoring jumps to a final reminder that points to the launcher. Configure this behavior from the panel:
FilamentTutorialsPlugin::make() ->dismissalReminder( skipLabel: __('Ignore'), title: __('You can return whenever you need'), description: __('Use the question-mark button to open this guide again.'), );
Disable it with ->withoutDismissalReminder().
Launcher Configuration
The launcher is rendered by default immediately before the Filament user menu. You may change its render hook, icon, tooltip, and accessible label:
use Filament\Support\Icons\Heroicon; use Filament\View\PanelsRenderHook; FilamentTutorialsPlugin::make() ->launcher( renderHook: PanelsRenderHook::USER_MENU_BEFORE, icon: Heroicon::OutlinedQuestionMarkCircle, tooltip: __('Open page guide'), label: __('Open page guide'), );
Shortcut methods are also available: launcherRenderHook(), launcherIcon(),
launcherTooltip(), and launcherLabel(). Disable the launcher with
->withoutLauncher().
Custom Discovery Directory
Override the default directory and namespace when the application's structure requires it:
FilamentTutorialsPlugin::make() ->discoverTutorials( in: app_path('Filament/App/ProductGuides'), for: 'App\\Filament\\App\\ProductGuides', );
Both in and for are required when providing a custom location. Disable class
discovery with ->discoverTutorials(enabled: false) and register tutorials
explicitly when full manual control is preferred.
Progress And Security
Progress is recorded per authenticated user, panel, and tutorial. The browser never sends a user id; the endpoint resolves the authenticated user server-side.
The runtime records:
startedwhen the tutorial starts;completedwhen the user finishes;dismissedwhen the user closes early;restartedwhen a completed or dismissed tutorial is opened again.
The progress endpoint uses the panel's authentication guard. Outside the local
environment, the authenticated model must implement Filament's FilamentUser
contract and pass canAccessPanel() for the requested panel.
The default middleware stack is:
['web', 'throttle:filament-tutorials-progress']
Keep the web middleware because the endpoint requires session authentication
and CSRF validation. The limiter defaults to 120 requests per 60 seconds per
client IP and can be adjusted in
filament-tutorials.progress.rate_limit.
Progress metadata is sanitized by allowlist before persistence. Do not place credentials, cookies, tokens, browser fingerprints, or portal session material in tutorial keys, targets, copy, or metadata.
If persistence fails, the tutorial remains usable and dispatches an event:
document.addEventListener('filament-tutorials:progress-failed', (event) => { const { event: progressEvent, status } = event.detail })
Troubleshooting
The launcher does not appear
Check these points in order:
- Confirm
FilamentTutorialsPlugin::make()is registered on the current panel. - Confirm the tutorial file is inside the directory derived from the panel id.
- Confirm the PHP namespace matches that directory exactly.
- Confirm the tutorial class extends
FilamentTutorial. - Confirm
$pageorforPage()references the Filament Page currently open. - Run
php artisan optimize:clearandphp artisan filament:assetsafter installing or updating the package.
The tutorial opens but a step cannot find its target
- Confirm the target helper and
TutorialTargetAttributesuse exactly the same action owner or component key. - Add the appropriate lifecycle helper when the target starts hidden.
- Mark the step
optional()only when the missing target is a valid page state. - Do not target Filament's generated
.fi-*classes or DOM position.
Registration fails with an invalid or duplicated key
Use lowercase stable keys containing only letters, numbers, ., _, and -.
Ensure tutorial keys are unique inside the panel and step keys are unique inside
their tutorial.
Package Development
The repository uses coringawc/filament-plugin-workbench:
./packages/workbench/bin/workbench up -d ./packages/workbench/bin/sail php vendor/bin/pest
Useful package gates:
./packages/workbench/bin/sail composer validate --strict ./packages/workbench/bin/sail composer audit ./packages/workbench/bin/sail npm run check ./packages/workbench/bin/sail npm audit --audit-level=moderate ./packages/workbench/bin/sail npm run build ./packages/workbench/bin/sail composer run build ./packages/workbench/bin/sail php vendor/bin/pest --compact tests/Architecture tests/Feature tests/Unit ./packages/workbench/bin/sail php vendor/bin/pest --compact tests/Browser ./packages/workbench/bin/sail php vendor/bin/phpstan analyse --memory-limit=1G ./packages/workbench/bin/sail php vendor/bin/rector process --dry-run --no-progress-bar ./packages/workbench/bin/sail pint --dirty --format agent