Type to search Tours, Hints, options, and events.

to navigate · Enter to open · Esc to close

Documentation

Authoring modes

Choose between declarative Twig Components, the Twig builder, and the PHP builder.

There are three ways to declare a Tour or a Hints group, and they produce the same serialized configuration for the same Stimulus controller: the <twig:Driver:Tour> component, the create_tour() Twig builder, and the autowired TourBuilder service in PHP. The choice is about where the help is easiest to read and maintain.

The same Tour, three ways

Component

<twig:Driver:Tour id="profile-tour">
    <button {{ ux_tour_action('start') }}>Start tour</button>

    <twig:Driver:Step :order="1" title="Profile" description="Keep your public details current">
        <section class="profile-card">...</section>
    </twig:Driver:Step>

    <twig:Driver:Step :order="2" title="Security" description="Review login settings" side="top">
        <section class="security-card">...</section>
    </twig:Driver:Step>
</twig:Driver:Tour>

Components put the help inside the template, wrapped around the element it describes. <twig:Driver:Step> and <twig:Driver:Hint> render ordinary elements carrying data-* attributes. The controller reads those attributes only when no builder Steps or Hints were passed — builder input wins.

Twig builder

{% set tour = create_tour('profile-tour')
    .addStep('.profile-card', 'Profile', 'Keep your public details current')
    .addStep('.security-card', 'Security', 'Review login settings', 'top') %}

<button {{ ux_tour(tour) }} {{ ux_tour_action('start') }}>Start tour</button>

create_tour() and create_hints() are Twig functions registered by the bundle; ux_tour() and ux_hints() print the controller attributes, ux_tour_action() and ux_hints_action() the trigger. The builder addresses elements by selector, so the Steps do not have to sit inside the markup they explain.

PHP builder

use Pentiminax\UX\Driver\Builder\TourBuilder;

public function __construct(private readonly TourBuilder $tourBuilder)
{
}

public function profile(): Response
{
    $tour = $this->tourBuilder->create('profile-tour')
        ->addStep('.profile-card', 'Profile', 'Keep your public details current')
        ->addStep('.security-card', 'Security', 'Review login settings', side: 'top');

    return $this->render('profile.html.twig', ['tour' => $tour]);
}

TourBuilder and HintsBuilder are autowired services — type-hint them in a controller or a service and Symfony injects them. The template then prints {{ ux_tour(tour) }} and {{ ux_tour_action('start') }} on the trigger element.

Because a Tour is a plain PHP object, this is the mode for Steps that depend on the current user, a feature flag, a repository query, or translations resolved in a service — and the only mode you can unit test without rendering a template.

Choosing

Reach for components when the target markup lives in the same template and you want the next developer to see the help while editing the page. Reach for the Twig builder when the Steps are assembled from template data or a macro. Reach for the PHP builder when the Steps are data-driven, conditional, or belong to a controller or a dedicated service, and the selectors are stable.

All three paths run through the same Tour, Step, Hints, and Hint model classes, so validation and escaping behave identically — the Twig builder is literally the PHP builder exposed as a Twig function. See Security for what gets escaped, and Dynamic targets for the one capability only builder Steps have.