CSS Transitions with useTransition

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

I want to add a CSS transition so that our search preview fades in and out, instead of showing up immediately. This may seem like a small detail, but this kind of stuff can be the difference between a mediocre interface and a truly beautiful experience.

Why CSS Transitions can be Tricky

So how can we do this? We learned some CSS transition basics earlier. In assets/styles/app.css, if you look for cart-item... we set transition: opacity 500ms. When that element also has a removing class, we set opacity to 0.

The result is that, as soon as we added this removing class to an element that already has the cart-item class, that element's opacity changed from 1 to 0 over 500 milliseconds.

So CSS transitions are simple, right?

Well... not always. If we repeated that trick here, we could transition the opacity of this element to 0. But then... the element would still technically be there... it would just be invisible. So if the user clicked right here, to them, it would look like they were clicking this photo. But in reality, they would be clicking the invisible dropdown above, which would take them to the disappearing ink pens page.

Ok then! So we just need to fade this element to opacity 0, wait 500 milliseconds for the transition to finish... and then fully hide the element. Right? Yea! That's exactly what we need to do! But sheesh, this is getting complicated! And if, later, we decided to change the transition in CSS from 500 milliseconds to 1 second, we would need to remember to go into our Stimulus controller and also change the delay there to 1 second... so that we don't hide the element before it's finished fading out.

So that is why CSS transitions are trickier than they seem at first. Fortunately, the stimulus-use library we installed earlier - to get behaviors like clickOutside and dispatch - has a solution for us!

Head to their docs and find a behavior called useTransition. This is a brand new feature that allows us to add CSS transitions when we need to hide or show an element. At the time of recording, this is currently a beta feature... which means it could change a bit - but I'll update the video if that happens.

Upgrading stimulus-use

The feature was introduced in version 0.24.0... and it's so new that I need to upgrade my stimulus-use to get it!

Find your terminal and run:

yarn upgrade stimulus-use@beta

I'm using @beta because the feature isn't included yet in a stable release at the time of this recording. The 0.24.0-1 version is a beta release. Once they release 0.24 stable or higher, you won't need that.

Open up the current search-preview_controller. As a reminder, as we type, this makes an Ajax call to an endpoint that returns the HTML of the search preview, which we then put into resultTarget. That's this element right here.

Initializing the Behavior

To activate the useTransition behavior, we'll do the same thing as the other behaviors. Import useTransition... and down in connect(), initialize it: useTransition(this).

... line 1
import { useClickOutside, useDebounce, useTransition } from 'stimulus-use';
... line 3
export default class extends Controller {
... lines 5 - 11
connect() {
... lines 13 - 14
useTransition(this, {
... lines 16 - 23
});
}
... lines 26 - 44
}

But this behavior has several required options. The first is called element. This is the element that we want to hide or show. For us, that's this.resultTarget.

I'm going to paste in the next six options.

... lines 1 - 11
connect() {
... lines 13 - 14
useTransition(this, {
element: this.resultTarget,
enterActive: 'fade-enter-active',
enterFrom: 'fade-enter-from',
enterTo: 'fade-enter-to',
leaveActive: 'fade-leave-active',
leaveFrom: 'fade-leave-from',
leaveTo: 'fade-leave-to',
hiddenClass: 'd-none',
});
}
... lines 26 - 46

These are CSS classes that we're going to create in our CSS file in a minute. Don't worry about them yet. The last option we need is hiddenClass set to d-none.

You'll see how this is used in a second, but d-none is a Bootstrap class that sets display: none on an element.

New enter(), leave() Methods

Before we talk about these options, now that we've initialized the useTransition() behavior, our controller has 3 new methods! Woo! They're called enter(), leave(), and toggle().

Down after we make the Ajax call, to show the resultTarget element, say this.enter().

Then, later, when we want to hide it, we can say this.leave(). And we will not clear the HTML anymore: we'll keep the HTML, but hide the element.

... lines 1 - 3
export default class extends Controller {
... lines 5 - 30
async search(query) {
... lines 32 - 38
this.enter();
}
... line 41
clickOutside(event) {
this.leave();
}
}

The Elements Hides and Shows!

Okay! That's all we need in the controller. If we do nothing else, the new behavior already gives us an easy way to hide or show an element without CSS transitions.

Let me show you. Reload the page, inspect element on the input and look at the result target element. Woh! It now has a d-none class! useTransition added that!

This happens thanks to the hiddenClass option that we passed. This tells useTransition which class to use to fully hide an element. And, by default, useTransition assumes that your element starts hidden. So as soon as our controller is initialized, it adds that class.

If you have a situation where your element starts visible instead of hidden, you'll need to add a transitioned option set to true... though that may be renamed to an option called initialState that you would set to enter. Check the docs to be sure.

Anyways, watch the element as I start typing. Boom! The d-none class is gone and the element is visible! But there was no transition yet: it all happened instantly. When we click off the element, the d-none is instantly added back. Like I said, if we do nothing else, this behavior gives us an easy way to hide or show an element.

So... how can we make this actually transition? That's up to us! Next, let's add the CSS needed to make the element fade in and fade out. We'll explore the lifecycle of how useTransition intelligently adds and removes classes at just the right time to make this all possible.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}