Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Symfony UX Stimulus Packages

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We can now create custom Stimulus controllers with ease. The other half of StimulusBundle is the ability to get more free Stimulus controllers by installing a UX package. Let's add one and see how it works!

Installing Turbo

Let's start by adding Turbo. At your terminal, say:

composer require symfony/ux-turbo

Here's the juiciest part: just like when we added Stimulus, there's absolutely nothing else you need to do to set this up. Refresh and... it just works! Turbo eliminates the need for full page refreshes. Head over to your Network tools and click on "Fetch/XHR". Let's actually clear this out so we can see everything. Perfect. Then, if we click up here... you can see that this is coming from an AJAX call! Yup, those full page refreshes are gone. So Turbo just works. There's no build system to get in the way, and that's beautiful.

UX Packages Often add Importmap Entries

In practice, this works because a new JavaScript file is being loaded called turbo_controller.js. Filter the network calls to JavaScript... and refresh, because I cleared them. There we go! Our page loads turbo_controller.js and that imports @hotwired/turbo, which starts Turbo.

Open up importmap.php. When we installed the UX Turbo package, its recipe added this new @hotwired/turbo entry.

34 lines importmap.php
... lines 1 - 15
return [
... lines 17 - 29
'@hotwired/turbo' => [
'url' => 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm',

This is a really common pattern with UX packages: if a UX package depends on a third-party package, its recipe will add that package to your importmap automatically. The result is that, when that package is referenced - like import '@hotwired/turbo' - it just works.

How UX Controllers are Loaded

The real question is: who's loading turbo_controller.js, which lives deep inside the symfony/ux-turbo PHP package?

The answer is: the same trick we learned a moment ago. Search for controllers and open that file in a new tab. This is the dynamic file that StimulusBundle builds. As it turns out, it looks for packages in our assets/controllers/ directory, which is these two, and it reads the assets/controllers.json file. When we installed UX Turbo, it added this new section here, which is where we activate different controllers. It activated one called turbo-core with "enabled": true and added another deactivated one with "enabled": false. So when this file is built, it parses the assets/controllers.json file, finds the controllers that we've enabled, and adds them here.

"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
"entrypoints": []

The final result is that it imports that controller file here and exports it so that the loader.js file can register it in Stimulus. So any controllers in assets/controllers/ or in this file are registered automatically.

autoimport & CSS Files

Head back into base.html.twig. When we installed StimulusBundle, its recipe came bearing gifts - one of which was this ux_controller_link_tags(). Right now, that does nothing. However, some UX packages come with CSS files. You'll find them under a key called autoimport, which the recipe will add under the controller. This ux_controller_link_tags() finds all the CSS files for all the controllers you have activated, and it outputs them. Nothing too fancy.

Next: let's learn one more thing about Stimulus, which just happens to be one of my favorite things: how to make our controllers lazy.

Leave a comment!

Login or Register to join the conversation
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 2 months ago

This is great, I didn't know about the ux_controller_link_tags().

Alas, when I try to use it I get

Unable to load the "Symfony\UX\StimulusBundle\Twig\UxControllersTwigRuntime" runtime

I think all my bundles and recipes are current, any idea where to look to solve this?



It looks like something is not fully configured, or maybe some services are cached. It will be better to double-check a version of StimulusBundle and maybe try to re-install or update it.

Did you get it on the course code?


Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | sadikoff | posted 2 months ago | edited

I've reinstalled symfony/stimulus-bundle, same issue.

I do have webpack encore installed, though -- could there be a conflict? Can both be used simultaneously?

{% block stylesheets %}
    {{ encore_entry_link_tags('app') }}
    {{ ux_controller_link_tags()  }}
{% endblock %}

there could be a version conflict, can you please share what exact version of bundles you have installed (stimulus one and encore)?


Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^4.0", // v4.2.0
        "doctrine/doctrine-bundle": "^2.7", // 2.10.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
        "doctrine/orm": "^2.12", // 2.15.2
        "knplabs/knp-time-bundle": "^1.18", // v1.20.0
        "pagerfanta/doctrine-orm-adapter": "^4.0", // v4.1.0
        "pagerfanta/twig": "^4.0", // v4.1.0
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.1
        "symfony/asset": "6.3.*", // v6.3.0
        "symfony/asset-mapper": "6.3.*", // v6.3.0
        "symfony/console": "6.3.*", // v6.3.0
        "symfony/dotenv": "6.3.*", // v6.3.0
        "symfony/flex": "^2", // v2.3.1
        "symfony/framework-bundle": "6.3.*", // v6.3.0
        "symfony/http-client": "6.3.*", // v6.3.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.3.*", // v6.3.0
        "symfony/runtime": "6.3.*", // v6.3.0
        "symfony/stimulus-bundle": "^2.9", // v2.9.1
        "symfony/twig-bundle": "6.3.*", // v6.3.0
        "symfony/ux-turbo": "^2.9", // v2.9.1
        "symfony/web-link": "6.3.*", // v6.3.0
        "symfony/yaml": "6.3.*", // v6.3.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.6.1
        "twig/twig": "^2.12|^3.0" // v3.6.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/debug-bundle": "6.3.*", // v6.3.0
        "symfony/maker-bundle": "^1.41", // v1.49.0
        "symfony/stopwatch": "6.3.*", // v6.3.0
        "symfony/web-profiler-bundle": "6.3.*", // v6.3.0
        "zenstruck/foundry": "^1.21" // v1.33.0