The post is a translation of the article “Improving User Flow Through Page Transitions” with Smashing Magazine about creating smooth transitions. The author of this article, Luigi De Rosa, is a front-end developer at EPIC . Further, the story will go on behalf of the author. Enjoy reading.
Every time a user experiences an interaction experience (UX), the chance of his leaving increases. Changing pages from one to the other often causes interruptions in the form of white blink with no content, causing a long load, or pulling the user out of the context of the page that was opened earlier.
Transitions between pages can enhance this experience by preserving (or even improving) the user's context, keeping their attention and providing a visual continuation. At the same time, transitions between pages can be pleasing to the eye and be interesting with good performance.
In this article, we will, step by step, create transitions between pages. We will also talk about the pros and cons of this technique and how to use it to the maximum.
Many mobile apps use excellent transitions between views. In this example below, which follows the recommendations of the Google material design, we see how the animation transmits the hierarchical and spatial relationships between the pages.
Why don't we take a similar approach to our websites? Why do we agree with the fact that the user feels as if he is teleported every time he changes the page?
Before getting our hands dirty, I have to say something about single-page application frameworks (SPA). If you are using a SPA framework (such as AngularJS, Backbone.js or Ember), then creating transitions will be much easier, because all the paths are processed by JavaScript. In this case, you should refer to the relevant documentation to look at the implementation of transitions between pages in the framework of your choice, because there may have good examples and instructions.
My first attempt to create a transition between pages looked like this:
document.addEventListener('DOMContentLoaded', function() { // Animate in }); document.addEventListener('beforeunload', function() { // Animate out });
The concept is simple: Use animation when the user leaves the page, and another animation when the new page loads.
However, I soon noticed that this solution has a number of limitations:
In fact, the only way to achieve a smooth transition is to gain complete control over the process of changing pages and, therefore, not to change the whole page.
Thus, we need to change the approach to the problem.
Let's take a look at the steps involved in creating a simple, smooth transition between pages in the right way. There is something here called pushState
AJAX (or PJAX) navigation, which essentially turns our site into something like a one-page site.
This is not only a method of achieving smooth and pleasant transitions, but we will also take advantage of some of the other advantages that we will cover in detail later in this article.
The first step is to create a click event handler for all links, preventing them from standard behavior and changing the way the page breaks are handled.
// , // , , . document.addEventListener('click', function(e) { var el = e.target; // , .href (HTMLAnchorElement) while (el && !el.href) { el = el.parentNode; } if (el) { e.preventDefault(); return; } });
This method of adding a handler to the parent element, instead of adding to a particular node, is called event delegation , which is possible due to the nature of the HTML bubble events of the DOM API.
Now that we have interrupted the browser loading of the page, we can manually get the page using the Fetch API . Let's look at the following function, which gets the contents of the HTML page when it receives its URL.
function loadPage(url) { return fetch(url, { method: 'GET' }).then(function(response) { return response.text(); }); }
For browsers that do not support the Fetch API, you should add a polyfill , or use XMLHttpRequest .
HTML5 has a fantastic API called pushState
, which allows websites to access and change browser history without loading any pages. Below we use this to change the current URL to the URL of the next page. Notice that this is a modification of the click handler announced earlier.
if (el) { e.preventDefault(); history.pushState(null, null, el.href); changePage(); return; }
As you can see, we also added a call to the changePage
function, which we will take a changePage
look at in more detail. A similar function will also be called in the popstate
event, which will occur when the active browser history is changed (for example, when the user presses the back button):
window.addEventListener('popstate', changePage);
Thus, we build a very primitive routing system in which we have active and passive modes.
Active mode occurs when the user clicks on the link, and we change the URL using pushState
, while passive mode occurs when the URL changes, and we receive a notification from the popstate
event. In any case, we are going to call changePage
, which will take care of reading our new URL and loading the page.
Usually, the pages on which the transition is carried out, have such basic elements as header and footer. We will use the following DOM structure on all of our pages (which, by itself, is the structure of Smashing Magazine):
<header> … </header> <main> <div class="cc"> … </div> </main> <footer> … </footer>
The only part that we need to change on each page is the contents of the cc
container. Thus, we can build our changePage
function changePage
this:
var main = document.querySelector('main'); function changePage() { // , URL var url = window.location.href; loadPage(url).then(function(responseText) { var wrapper = document.createElement('div'); wrapper.innerHTML = responseText; var oldContent = document.querySelector('.cc'); var newContent = wrapper.querySelector('.cc'); main.appendChild(newContent); animate(oldContent, newContent); }); }
When a user clicks on a link, the changePage
function changePage
HTML of this page, then extracts the cc
container and adds it to the main
element. At the moment we have two containers cc
on our page, the first belongs to the previous page, and the second to the next.
The next function, animate
, takes care of smoothly overlapping the two containers, hiding the old one, revealing the new one and removing the old container. In this example, I use the Web Animations API to create an appearance animation, but you can use any other method or library that you like.
function animate(oldContent, newContent) { oldContent.style.position = 'absolute'; var fadeOut = oldContent.animate({ opacity: [1, 0] }, 1000); var fadeIn = newContent.animate({ opacity: [0, 1] }, 1000); fadeIn.onfinish = function() { oldContent.parentNode.removeChild(oldContent); }; }
The resulting code is available on Github .
All this is just the basics of navigating through the pages!
The small example we just created is far from ideal. In fact, we did not take into account a number of things:
target="_blank"
(which opens the page in a new tab), all links to external domains, and some other special cases, such as Control/Command + click
(which also open the page in a new tab).cc
container remain the same. However, some of these elements need to be changed (now it is only possible to change it manually), including the title
document, the menu element with the class active
and potentially many other dependencies on our site.The only requirement for this navigation mode is the pushState
API, which is available in all modern browsers . This method works completely as a progressive improvement . Pages are still available in the usual way, and the website will continue to function normally if JavaScript is disabled.
If you are using a SPA framework, consider using PJAX navigation instead, to speed up navigation. In return, you will get support from older browsers and create a SEO-friendly website.
We can continue to squeeze the most out of this way by optimizing some aspects. The following pair of tricks will speed up navigation, greatly improving the user experience.
loadPage
slightly changing our loadPage
function, we can add a simple cache that will make sure that pages that have already been visited will not be loaded again.
var cache = {}; function loadPage(url) { if (cache[url]) { return new Promise(function(resolve) { resolve(cache[url]); }); } return fetch(url, { method: 'GET' }).then(function(response) { cache[url] = response.text(); return cache[url]; }); }
As you might have guessed, we can use a more durable cache with the Cache API or other persistent storage on the user's side (for example, IndexedDB).
Our damping effect requires the next page to be loaded and ready before the transition is completed. We would like to start the animation on the old page immediately after clicking on the link, which will give the user instant responsiveness and perception of performance.
Using promises , handling such situations can seem very simple. The .all
method creates a new promise that will be fulfilled after all promises passed in the form of arguments are fulfilled.
// animateOut() loadPage() Promise.all[animateOut(), loadPage(url)] .then(function(values) { …
Using PJAX navigation, the page is replaced almost twice as fast as the default navigation, because the browser does not have to parse and calculate any scripts or styles on a new page.
However, we can go further by starting to preload the next page when the user hovers over the link.
As you can see, the delay between clicking and hovering the cursor is usually 200 to 300 milliseconds. This time is usually enough to load the next page.
But it can easily go sideways. For example, if you have a long list of links, and the user scrolls through the page through them, this method will preload all the pages, because the links appear under the cursor.
Another point that we may have noticed and taken into account is the prediction of the speed of the user's connection. (Perhaps this will be possible in the future with the Network Information API .)
In our loadPage
function loadPage
we get the entire HTML document, although we only need the cc
container. If we were using the language on the server side, we would be able to detect if a request comes from a particular user-defined AJAX call and, if so, only display the correct container. Using the Headers API, we can send a custom HTTP header in our request.
function loadPage(url) { var myHeaders = new Headers(); myHeaders.append('x-pjax', 'yes'); return fetch(url, { method: 'GET', headers: myHeaders, }).then(function(response) { return response.text(); }); }
Then, on the server side (using PHP in this case), we can determine if our custom header exists before outputting the required container:
if (isset($_SERVER['HTTP_X_PJAX'])) { // }
This will reduce the size of HTTP messages, as well as reduce the load on the server.
After the introduction of this technique in a number of projects, it seemed to me that a reusable library would be extremely useful. This would save time the next time, allowing you to focus on the transition effects themselves.
Thus, Barba.js was born - a small library (4 KB in compressed state), which abstracts all this complexity and provides a pleasant, clean and simple API for developers. It also takes into account different views and comes with ready-made transitions, caching, preloading and events. The library has open source code and is available on GitHub .
We have just looked at how to create a smooth overlap effect, the pros and cons of using PJAX navigation to effectively turn our website into a SPA. Besides the advantages of the transition itself, we also considered the introduction of simple caching and preloading mechanisms to speed up the loading of new pages.
This whole article is based on my personal experience and what I learned during the implementation of transitions in the projects I worked on.
Source: https://habr.com/ru/post/305300/
All Articles