First Tour
Build a product tour with Twig Components or the PHP builder, and start it from a button.
A Tour is an ordered list of Steps. Each Step points at one element and carries the popover text shown when the Tour reaches it. Here is the same three-Step Tour over an orders screen, written in each of the three authoring forms — a Twig Component tree, the Twig builder, and the PHP builder in a controller.
Component
<twig:Driver:Tour id="orders-tour" :once="true">
<button type="button" {{ ux_tour_action('start') }}>
Start tour
</button>
<twig:Driver:Step :order="1" title="Orders" description="Review current customer orders">
<h1>Orders</h1>
</twig:Driver:Step>
<twig:Driver:Step :order="2" title="Filters" description="Narrow the list before export" side="bottom">
<form class="filters">...</form>
</twig:Driver:Step>
<twig:Driver:Step :order="3" title="Done" description="Export the filtered result" side="left">
<button type="button">Export</button>
</twig:Driver:Step>
</twig:Driver:Tour>Each <twig:Driver:Step> wraps the element it describes, so the Tour reads top to bottom like the page does.
Twig builder
{% set tour = create_tour('orders-tour')
.addStep('.orders-title', 'Orders', 'Review current customer orders')
.addStep('.filters', 'Filters', 'Narrow the list before export')
.addStep('.export', 'Done', 'Export the filtered result', 'left')
.once() %}
<button type="button" {{ ux_tour(tour) }} {{ ux_tour_action('start') }}>
Start tour
</button>The builder addresses elements by selector, so the Steps do not have to sit inside the markup they explain.
PHP builder
namespace App\Controller;
use Pentiminax\UX\Driver\Builder\TourBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class OrderController extends AbstractController
{
public function __construct(private readonly TourBuilder $tourBuilder)
{
}
#[Route('/orders', name: 'orders')]
public function index(): Response
{
$tour = $this->tourBuilder->create('orders-tour')
->addStep('.orders-title', 'Orders', 'Review current customer orders')
->addStep('.filters', 'Filters', 'Narrow the list before export')
->addStep('.export', 'Done', 'Export the filtered result', side: 'left')
->once();
return $this->render('order/index.html.twig', ['tour' => $tour]);
}
}TourBuilder is an autowired service, so the constructor above is all the wiring there is. The template
prints the same two attributes as the Twig builder:
<button type="button" {{ ux_tour(tour) }} {{ ux_tour_action('start') }}>
Start tour
</button>Build the Tour in PHP when the Steps depend on the user, the data, or a feature flag — the whole
Tour object is ordinary PHP, so it can be assembled in a service and unit-tested.
Both modes serialize the same Driver.js configuration for the same Stimulus controller. Authoring modes covers how to choose.
Starting the Tour
Tours wait for an explicit trigger. Either put ux_tour_action('start') on a control inside the Tour, as above:
<button {{ ux_tour_action('start') }}>Start tour</button>
…or set :autostart="true" and let the Tour run when the controller connects.
Tuning the popovers
showProgress is already on for Tours declared through UX Driver, so 1 of 3 appears without asking.
Everything else — button labels, overlay color, stage padding, placement — is a Tour or Step option:
- Tour reference — the 20-odd options that apply to the whole walkthrough.
- Step reference — per-Step placement, buttons, and async targets.
- Styling and localization — translated labels and custom popover CSS.