Shared elements transitions for Web
This blog post is heavily inspired by posts from Jiří Otáhal(post) and Narendra N Shetty(post), so big thanks to them before I begin. Also while these two posts focus on react-native, my post is to do the same and tackle the problems in this area on web.
I’ve always been fascinated with such shared element transitions across native apps realm. They’re fluid, eye catching and also transition the user’s attention very well to the next route. I also saw the same happening for react-native, because of its amazing animation API and the peer pressure from native platforms(I guess). But when it comes to web why isn’t anyone doing it? I tried to dig in, find the tough parts and sketch a pattern for building routes that could make transition routing super easy and scalable for SPAs to create and maintain.
For those who are new to shared element transitions, they are animations of an element from one route to another; while moving to a different route/screen/page etc e.g.: https://bit.ly/2HVHtk7
A lot of people I talked to, also had a thinking that since it’s already once built with react-native it should be easy to port. That is somewhat true as you can re-use your code, but there can be some rough edges if you think about how a browser behaves. Web ergonomics is different in many ways as compared to a native app, e.g.
- Your user can enter in your app from literally any route which is less likely(but not impossible for native apps) , so you need to be ready for a shared element transition or a normal page appearance at all routes. 🤷
- Back button 😢. Unlike native apps, you don’t get to control back button’s behavior. Hence, you will need to be prepared to do exit animation even then your route/app doesn’t know about it.
- Lazy loading 😕. You know your web app is not installed from a play-store/App store, it’s something that user downloads on runtime. Although this is one of the greatest power of web, this can cause erratic and unseen behavior for shared element transitions if the route is not in memory to transition into.
In this blog I’ll take on similar kind of challenge and would like to write a small 3 route app that should make sense to be extensible to be a part of a complete bigger app, and not just a demo thing.
By this means, I’ll try to be generic in my approach so that this may apply to more and more frameworks/libs and not just one and also will incorporate more realistic challenges like: retaining scroll positions, ability to start from any route instead of just one etc.
Here are two sample animations that we will rebuild for web in this blog post
How will we do the shared element
Every shared element transition is a three part animation in the pattern we’ll follow.
- User taps on a card and the existing route will handover it and its position to the GhostLayer* class, as well as disappears the card which was tapped on.
- GhostLayer will render the SharedElement on the given position.
- As the new Route mounts/initializes, it will give the destination position to the GhostLayer, which will return a Promise. This promise will be resolved when the animation of SharedElement to the destination will complete.
After this the new route will show the SharedElement’s clone on itself and GhostLayer will remove the SharedElement.
*GhostLayer: Imagine this as a div with highest z-index covering all of the screen always, which is active only when SharedElement is present in it.
Entry Animation
To do the SharedElement animation we will do entry animation on every route. Every entry animation will begin after adding a class to the route, this will give us the flexibility of deciding when to start the entry animation.
In case of direct entry on the page, we will immediately start the animation and in case of SharedElement transition we will wait for the animation on the GhostLayer to finish.
The way I will do it in my app is that, I will send the destination position of the SharedElement and GhostLayer will return an animation promise. Upon resolution of this promise, the entry animation of the rest of the page will be played.
With the above setup, we are good with every page being entry page independent of whether the app is launching from it, or the page is a part of SharedElement transition.
Tip: It might be a good idea to add
contain: strict
to avoid triggering any layout trigger fromgetBoundingClientRect
Exit Animation
Exit animation on mobile web may take a little more work than the other parts because of the back button we discussed. The problem arise when instead of tapping a UI element, user presses the back button.
In this case we don’t know which element should be chosen as the SharedElement. In this case I usually take a look at the next url and see if it can tell me about the SharedElement. e.g. If the next URL is ‘/details/stock3’, then I can predict that third stock card will be the SharedElement and share its clone to the GhostLayer.
If you’ve cracked the above part, rest of the flow should be kept similar to entry animation. i.e. Add a class that performs exit animation, share the element to the GhostLayer, and let the router mount the new route.
App Code
Using the above two discussed stuff, we’ll create a three route app which will do shared element transition amongst it.
I am using preact ecosystem/ preact-cli to bootstrap a project and will take it on from there.
preact create material transition-challenge
After that ☝, I’ll start implementing the routes and extend the router preact-router
with preact-transition-group
to add a callback ComponentWillLeave
which will tell the Component before it leaves where the exit animation can be started.
The above code shows the extra div which will work as our GhostLayer. I also implemented GhostLayer and implemented the runAnimation and addSharedElement methods.
Now we just need to connect this to a every route’s componentDidMount and componentWillLeave e.g.
Result:
Here’s a preview of what I was able to build with this generic pattern altogether.
Summary:
So here’s a summary sketch of the pattern I followed to to build this sample app.
Extension over this pattern
Feel free to extend this pattern by passing a map of element’s clone and their positions instead of just one element and its position. That way you’ll be able to share more than one element transition.
Github: github.com/prateekbh/transition-challenge
Working app: https://transition.surge.sh
Hope you liked it. For any questions feel free to ping me at twitter.