Dynamic targets
Point Steps at elements that appear late, or never appear at all.
A selector-based Step assumes its target is in the DOM when the Tour reaches it. Turbo Frames, modals, and lazily rendered panels break that assumption. Driver.js 1.8 gives two Step options for it, and UX Driver passes both through.
Wait for an element
waitForElement takes a timeout in milliseconds. The Tour pauses on that Step until the selector matches
or the timeout expires:
Twig builder
{% set tour = create_tour('invoice-tour')
.addStep('#open-invoice', 'Invoices', 'Open one to see the preview')
.addStep('#invoice-preview', 'Preview', 'This renders after the modal opens', options: {
waitForElement: 2000,
}) %}
<button type="button" {{ ux_tour(tour) }} {{ ux_tour_action('start') }}>
Start tour
</button>PHP builder
$tour = $this->tourBuilder->create('invoice-tour')
->addStep('#open-invoice', 'Invoices', 'Open one to see the preview')
->addStep('#invoice-preview', 'Preview', 'This renders after the modal opens', options: [
'waitForElement' => 2000,
]);Skip a missing target
skipMissingElement lets the Tour move on rather than stall when a selector never matches — the shape for
Steps that only exist for some roles:
Twig builder
{% set tour = create_tour('admin-tour')
.addStep('.orders', 'Orders', 'Every account sees this')
.addStep('.export', 'Export', 'Only admins see this control', options: {
skipMissingElement: true,
}) %}
<button type="button" {{ ux_tour(tour) }} {{ ux_tour_action('start') }}>
Start tour
</button>PHP builder
$tour = $this->tourBuilder->create('admin-tour')
->addStep('.orders', 'Orders', 'Every account sees this')
->addStep('.export', 'Export', 'Only admins see this control', options: [
'skipMissingElement' => true,
]);In PHP the alternative is to not add the Step at all — if ($this->isGranted('ROLE_ADMIN')) around the
addStep() call is often clearer than shipping a Step the Tour then has to skip.
Without it, a missing target leaves the Tour waiting on that Step with nothing to highlight.
Refresh after a layout shift
Neither option helps once a Tour is already running and the page moves under it — a Turbo render, a modal
animation, a lazily loaded image. refresh recomputes the overlay and popover position against the
current DOM:
<button {{ ux_tour_action('refresh') }}>Refresh tour</button>
<button {{ ux_hints_action('refresh') }}>Refresh hints</button>
Hints
Hints expose neither waitForElement nor skipMissingElement through this bundle. Either render Hint
targets before the Hints controller connects, or start the group with :autostart="false" and fire
show once the targets exist. Control and persistence has both
actions.