Animations are an essential part of web design and development. They can draw attention, guide users on their journey, provide satisfying and meaningful feedback to interaction, add character and flair to make the website stand out, and so much more!
Before we begin, let’s take a quick look at the following video and imagine how much CSS and JavaScript would take to create an animation like this. Notice that the cart counter is also animated, and the animation runs right after the previous one completes.
JavaScript libraries like React and Vue probably popped into your mind right away, alongside JavaScript animation libraries like Framer Motion and GSAP. They do a solid job and certainly simplify the process. However, JavaScript is the most expensive resource on the web, and these libraries are bundled alongside the content and primary resources. In the end, it’s up to the browser to download, parse and execute the animation engine before it’s ready for use.
What if we could just skip all that, use vanilla JavaScript and CSS, and let the optimized browser API do all the heavy lifting while maintaining complete control over how they perform transitions between the various UI states? With the new Shared Element Transitions API, implementing animations like this will become incredibly easy and seamless.
In this article, we’ll dive deeply into this game-changing API which is still in its early stages, and explore its incredible potential by building four fun and exciting real-life examples from scratch.
Browser Support And API Status
At the time of this article, API is in its early “Editor’s draft” stage, so the standard is not yet finalized, and the specs might change as time goes on.
Shared Element Transitions API is currently supported only in Chrome version 104+ and Canary with the document-transition flag enabled. Examples will be accompanied by a video, so you can easily follow along with the article if you don’t have the required browser installed.
Shared Element Transitions API
Animating between UI states usually requires both the current and the next state to be present at the same time. Let’s take a simple image carousel that crossfades between the images as an example. We’ll even create that carousel later. If you were using only JavaScript without any libraries or frameworks, you’d have to ensure that both the current and next image are present, and then fade out the current image and fade in the next image simultaneously. You also have to handle a handful of edge cases and accessibility issues that might come up as a result of that.
In a nutshell, Shared Element Transitions API allows us to skip a lot of prep work by ensuring that both outgoing and incoming states are visually present
at the same time. All we have to do is take care of the DOM updates and
animation styles. The API also allows us to tap into those individual
states and more and gives us full control over the animation using the
standard CSS animation
properties.
Let’s start with a simple and fun example. We’ll create an image gallery with the following animation - on card click, the image inside the card will expand and move from its place in the grid into a fixed overlay. When the expanded image in the overlay is clicked, it will return back into its place. Let’s see how we can create this animation in just a few lines of JavaScript and CSS.
Starting Markup, Styles And JavaScript
Let’s start with the following markup and some basic grid, card, and overlay styles.
Let’s take a closer look at our JavaScript code:
On
card click, we move the image element from the grid markup into the
overlay, leaving the container node empty. Visually, we are applying background-image
CSS to the empty container to create an illusion that the image is
still there. We could have achieved the same effect with the duplicated
image element with the optimal loading strategy, but I’ve chosen this
approach for simplicity.
We are also setting the overlay onclick
event, which moves the image back into its origin
container, and are also toggling the visibility CSS class on the overlay element.
It’s important to note that we are moving the image element from the grid HTML element into the overlay HTML element, so we have the same DOM node in the grid and the overlay. We can refer to the image as a “Shared Element”.
Adding A Simple Crossfade Transition
Now that we have finished setting up the markup, basic styles, and JavaScript functionality, we are going to create our first state transition using the Shared Element Transitions API!
Let’s go into our toggleImageView
function. First, we need to create a transition using the global createDocumentTransition
function. And then, we just need to pass the callback function that updates the DOM to the start
function:
By changing just a few lines of code and calling our DOM update functions via the API, we got this neat little crossfade animation right out of the box. All we had to do was to make sure that the DOM updated and called the required function.
When we call the start
function, the API takes the outgoing screenshot of the page state and performs the DOM update. When the update completes, the second, incoming image is captured. It’s important to point out that what we see during the transition are screenshots of the elements, not the actual DOM elements. That way, we can avoid any potential accessibility and usability issues during the transition.
By default, Shared Element Transitions API will perform a crossfade animation between these outgoing (fade-out) and incoming (fade-in) screenshots. This comes in handy as the browser ensures that both states are available for the entire animation duration, so we can focus more on customizing it!
Although this animation looks alright, it’s just a minor improvement. Currently, the API doesn’t really know that the image (shared element) that is being moved from the container to the overlay is the same element in their respective states. We need to instruct the browser to pay special attention to the image element when switching between states, so let’s do that!
Creating A Shared Element Animation
With page-transition-tag
CSS property, we can easily tell the browser to watch for the element in both outgoing and incoming images, keep track of element’s size and position that are changing between them, and apply the appropriate animation.
We also need to apply the contain: paint
or contain: layout
to the shared element. This wasn’t required for the crossfade animations, as it’s only required for elements that will receive the page-transition-tag
. If you want to learn more about CSS containment, Rachel Andrew wrote a very detailed article explaining it.
Another important caveat is that page-transition-tag
has to be unique, and we can apply it to only one element during the duration of the animation.
This is why we apply it to the active image element right before the
image is moved to the overlay and remove it when the image overlay is
closed and the image is returned to its original position:
Alternatively, we could have used JavaScript to toggle the page-transition-tag
property inline on the element. However, it’s better to use the CSS class toggle
to make use of media queries to apply the tag conditionally:
And that’s pretty much it! Let’s take a look at our example with the shared element applied:
It looks much better, doesn’t it? Again, with just a few additional lines of CSS and JavaScript, we managed to create this complex transition between the two states that would otherwise take us hours to create.
We’ve instructed the browser to watch for size and position changes between the UI states by tagging our shared active image element with a unique id and applying special animation. This is the default behavior for elements with the page-transition-tag
set. In the next few examples, we’ll learn how to customize animation properties other than opacity, position, and size.
Customizing Animation Duration And Easing Function
We’ve created this complex transition with just a few lines of CSS and JavaScript, which turned out great. However, we expect to have more control over the animation properties like duration, easing function, delay, and so on to create even more elaborate animations or compose them for even greater effect.
Shared Element Transitions API makes use of CSS animation
properties
and we can use them to fully customize our state animation. But which
CSS selectors to use for these outgoing and incoming states that the API
is generating for us?
Shared Element Transition API introduces new pseudo-elements that are added to DOM when its animations are run. Jake Archibald explains the pseudo-element tree in his Chrome developers article. By default (in case of crossfade animation), we get the following tree of pseudo-elements:
These pseudo-elements may seem a bit confusing at first, so I’m including WICG’s concise explanation for these pseudo-elements and their general purpose:
::page-transition
sits in a top-layer, over everything else on the page.::page-transition-outgoing-image(root)
is a screenshot of the old state, and::page-transition-incoming-image(root)
is a live representation of the new state. Both render as CSS replaced content.::page-transition-container
animates size and position between the two states.::page-transition-image-wrapper
provides blending isolation, so the two images can correctly cross-fade.::page-transition-outgoing-image
and::page-transition-incoming-image
are the visual states to cross-fade.
For example, when we apply the page-transition-tag: active-image
, its pseudo-elements are added to the tree:
In
our example, we want to modify both the crossfade (root) animation and
the shared element animation. We can use the universal selector *
with the pseudo-element to change animation properties for all
available transition elements and target pseudo-elements for specific
animation using the page-transition-tag
value.
In this example, we are applying 400ms
duration for all animated elements with an ease-in-out
easing function, and then override the active-image
transition easing function and setting a custom cubic-bezier
value:
Feature Detection And Fallbacks #
We got slightly carried away and made this example unusable on browsers that don’t support the API, so let’s fix that. We need to add some feature checks and fallbacks to make sure that the website remains usable for all users.
Feature detection in JavaScript is as simple as checking if the createDocumentTransition
function exists:
In CSS, we can use the @supports
tag to apply styles based on feature support conditionally. This is
useful when you want to provide a less ideal but still effective
standard CSS transition or animation:
If we need to support even older browsers, we can conditionally apply the CSS class using JavaScript:
We can check out our example on a browser that doesn’t support the API and see that the function still runs but is not animated.
Accessible Animations
It’s important to be aware of accessibility requirements when working with animations. Some people prefer browsing the web with reduced motion, so we must either remove an animation or provide a more suitable alternative. This can be easily done with a widely supported prefers-reduced-motion media query.
The following code snippet turns off animations for all elements using the Shared Element Transitions API. This is a shotgun solution, and we need to ensure that DOM updates smoothly and remains usable even with the animations turned off:
To-do List With Custom Animations
Now
that we got familiar with the basics of the Shared Element Transitions
API, we can move on to more complex examples and focus on composing
animations and utilizing CSS animation
properties and @keyframe
to fully customize the animations.
Let’s create an interactive to-do list with 3 column containers: tasks in progress, completed tasks, and won’t-do tasks. We’ll use a similar approach as before and customize the animation to make the motion more natural and bouncier. Clicked item will subtly scale up as it leaves the parent container and will scale back down and slightly bounce when it moves to a target container. The remaining to-do list elements should also be animated smoothly to move over to the empty space.
Starting Markup, Styles And JavaScript
We’ll use a very similar approach with moving elements around as in the previous example, so we’ll start with the Shared Element API already implemented with a default crossfade animation.
Let’s dive right into the JavaScript code. We have a similar setup here as in the previous example. The only difference is that we can have two possible target containers for our items in the to-do list: the second (“Done”) or the third column (“Won’t-do”).
Notice how the Shared Element Transitions API freezes rendering, and we cannot interact with any other element on the page while the animation is running. This is an important limitation to keep in mind, so it’s best to avoid creating lengthy animations that might harm usability and annoy users.
Creating Shared Element Transition
Let’s start by adding page-transition-tag
values to our card elements and setting up a basic position animation.
We’ll use two different sets of page-transition-tag
values for this example:
card-active
: for an element that is currently being moved to another column. We’ll apply this tag right before the animation runs and remove it once it ends;card-${index + 1}
: for other elements which are being moved within the first column to fill the empty space left by an animated card. They’ll have a unique tag based on their index.
Of course, we also need to add a contain
property to our cards which are list elements in our example. Without
the property, the animation wouldn’t break, but it would fall back to
the default crossfade animation.
Note: In the future, this behavior might change, and the DOM update will occur without the animation.
And,
just like that, we have a really nice position animation set up for our
card elements. Notice how we are doing nothing more than just toggling page-transition-tag
values and applying the contain: paint
value in our CSS, and we’re getting all these delightful animations.
This animation looks alright, but it feels somewhat rigid in this example. What gives? Sometimes the animation will look impressive right out of the box, like in the previous example. Other times, however, it will require some tuning. It usually depends on the motion, animation type, target look and feel, and purpose of the animation.
Creating A Scaling And Bouncing Animation #
We can take full advantage of CSS animation
properties and create our custom keyframes for animation pseudo-elements to achieve the bouncing effect.
First, we’ll slightly increase the animation-duration
value of the position animation. Even though ::page-transition-container
animates the size and position, we are using its direct child ::page-transition-image-wrapper
for scaling animation, so we don’t override the default positioning and
size animation. We are also slightly delaying the scale animation just
to make it flow better.
And finally, we are adjusting the animation-duration
value for all elements so everything remains in sync, and we are applying a custom cubic-bezier
timing function to achieve the final bouncing effect:
With just a few minor tweaks and a custom animation keyframe definition, we’ve created a delightful and whimsical animation to make our to-do example a bit more exciting.
Image Carousel With Custom Crossfade Animation
Let’s move on to a simpler example in which we’ll keep the focus on creating custom animation keyframes for both outgoing and incoming images.
We’ll create a crossfading image carousel. This is the first idea that popped into my mind when I saw the Shared Element Transitions API announcement video. Last year, I built a web app to help me keep track of my music collection, which displays a details page for each release. On that page, I’ve added a simple image carousel that shows images of an album cover, booklet, and packaging.
I grabbed the source code and simplified it for this example. So, let’s create a slightly more elaborate crossfade animation for this element and play around with CSS filters.
Photosensitivity warning: This
example involves animating image brightness to mimic a lightning flash.
If this applies to you, skip the following video and embedded examples.
You can still follow the example and create custom crossfade transition
by omitting brightness
values from CSS.
Starting Markup, Styles And JavaScript #
In
this example, we’ll showcase how we can run Shared Element Transitions
API to animate even the smallest changes, like changing a single HTML
property. As a proof of concept, we’ll have a single image HTML element,
and we’ll change its src
property as active image changes:
Let’s check out our JavaScript file. Now we have two functions, namely for moving backwards and forwards in the image array. We are running the same crossfade animation on both functions, and everything else is pretty much the same as in previous examples:
We’ll start with default crossfade animation, so we can focus more on the CSS and customizing animation properties.
Applying Custom Keyframes
Let’s make this crossfade animation a bit more elaborate by playing around with custom animation keyframes. We are creating a crossfade animation that imitates a lightning flash.
Let’s create the following keyframes:
fadeOut
: the current image will become blurrier and increase in brightness as its opacity value animates from1
to0
;fadeIn
: the next image will become less blurry and decrease in brightness as its opacity value animates from0
to1
.
Now,
all we have to do is assign the exit animation to the outgoing image
pseudo-element and the entry animation to the incoming image
pseudo-element. We can set a page-transition-tag
directly to the HTML image element as it’s the only element that will perform this animation:
Even the seemingly simple crossfade animations can look cool, don’t you think? I think this particular animation fits really nicely with the dark theme we have in the example.
Elaborate Add-to-cart Animation
In our final example, we’ll build the project we introduced in the very first video in this article. We finally get to build a neat add-to-cart animation!
In our previous examples, we ran a single custom animation on a single element. In this example, we’ll create two animations that run one after another. First, the click event will activate the dot that moves to the cart icon, and then the cart counter value change will be animated.
Regardless of animation complexity, Shared Element Transitions API streamlines the whole process and hands us complete control over the look and feel of the animation.
Starting Markup, Styles And JavaScript
As in the previous examples, we’ll start with a basic markup and style setup and a basic crossfade animation:
Creating Composed Animations
First, we need to toggle a page-transition-tag
value for our dynamic dot
element and the cart
element in their respective transition animations:
Next, we’ll customize both animations with CSS. For the dot
animation, we’ll change its duration and timing function, and for cart-counter
, we’ll add a slight vertical movement to a standard crossfade animation.
Notice how the dot
element doesn’t have animations or CSS properties for changing
dimensions. Its dimensions respond to the parent container. Its initial
position is in the button container, and then it is moved to a smaller cart
icon container, which is located behind the counter. Shared Elements
API understands how element position and dimensions change during the
animation and gives us this elegant transition animation right out of
the box!
And
that is it! It amazes me every time how elaborate these animations can
turn out with so few lines of additional code, all thanks to Shared
Element Transitions API. Notice that the header
element with the cart
icon is fixed, so it sticks to the top, and our standard animation setup works like a charm, regardless!
Conclusion
When done correctly, animations can breathe life into any project and offer a more delightful and memorable experience to users. With the upcoming Shared Element Transitions API, creating complex UI state transition animations has never been easier, but we still need to be careful how we use and implement animations.
This simplicity can give way to bad practices, such as not using animations correctly, creating slow or repetitive animations, creating needlessly complex animations, and so on. It’s important to learn best practices for animations and on the web so we can effectively utilize this API to create truly amazing and accessible experiences or even consult with the designer if we are unsure on how to proceed.
In the next article, we’ll explore the API’s potential when it comes to transition between different pages in Single Page Apps (SPA) and the upcoming Cross-document same-origin transitions, which are yet to be implemented.
I am excited to see what the dev community will build using this awesome new feature. Feel free to reach out on Twitter or LinkedIn if you have any questions or if you built something amazing using this API.
Go ahead and build something awesome!
Many thanks to Jake Archibald for reviewing this article for technical accuracy.
No comments:
Post a Comment