Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Friday, October 30, 2020

Building Your Security Strategy (Case Study)

 In this article, Wix security experts share ten “security by design” principles that emerged from their work in keeping the Wix platform secure. If you’re a developer, these tried-and-true principles can help you build your own secure applications.

What should you focus on when designing your security strategy? This question becomes more and more tricky as your organization grows and matures. At an initial stage, you might be able to make due with a periodic penetration test. But you will soon find that as you scale up to hundreds and thousands of services, some of the procedures have to change. The focus shifts from project-based assessments to building and maintaining a lasting mindset and framework with security at the core, so you can minimize risk across your environment.

In this article, we’ll share some guiding principles and ideas for incorporating security by design into your own development process, taken from our work at Wix serving 220M+ users.

First And Foremost: Security By Design

Also known as security by default, security by design (SbD) is a concept in which we aim to “limit the opportunities” for making security-related mistakes. Consider a case where a developer builds a service to query a database. If the developer is required (or allowed) to build queries “from scratch” writing SQL directly into his code, they can very well end up introducing SQL Injections (SQLI) vulnerabilities. However, with a security by default approach, the developer can get a safe Object-Relational Mapping (ORM), letting the code focus on logic where the DB interactions are left for the ORM libraries. By ensuring the ORM library is safe once, we are able to block SQLI everywhere (or at least everywhere the library is used). This approach might restrict some developer liberties, but except for specific cases, the security benefits tend to outweigh the cons.

That previous example is rather well known, and if you use a mature application development framework, you’re probably using an ORM anyway. But the same logic can be applied to other types of vulnerabilities and issues. Input validation? Do this by default using your app framework, according to the declared var type. What about Cross-Site Resource Forgery (CSRF)? Solve it for everyone in your API gateway server. Authorization confusion? Create a central identity resolution logic to be consumed by all other services.

By following this methodology, we’re able to allow our developers the freedom to move quickly and efficiently, without needing to introduce security as a “blocker” in later stages before new features go live.

1. Establish Secure Defaults For Your Services

Take the time to ensure that your services are served by default with secure settings. For example, users should not need to actively choose to make their data private. Instead, the default should be “private” and users can have the option to make it public if they choose to. This of course depends on product decisions as well, but the concept stands. Let’s look at an example. When you build a site on our platform, you can easily set up a content “Collection”, which is like a simplified database. By default, editing permissions to this collection are restricted to admin users only, and the user has the option to expose it to other user types using the Roles & Permissions feature. The default is secure.

2. Apply The Principle Of Least Privilege (PoLP)

Put simply, users shouldn’t have permission for stuff they don’t need. A permission granted is a permission used, or if not needed, then abused. Let’s look at a simple example: When using Wix, which is a secure system with support for multiple users, a website owner can use Roles & Permissions to add a contributor, say with a Blog Writer role, to their site. As derived from the name, you would expect this user to have permissions to write blogs. However, would this new contributor have permissions, for example, to edit payments? When you put it like this, it sounds almost ridiculous. But the “least permission” concept (PoLP) is often misunderstood. You need to apply it not only to users, but also to employees, and even to systems. This way even if you are vulnerable to something like CSRF and your employees are exploited, the damage is still limited.

In a rich microservice environment, thinking about least permission might become challenging. Which permission should Microservice A have? Should it be allowed to access Microservice B? The most straightforward way to tackle this question is simply starting with zero permissions. A newly launched service should have access to nothing. The developer, then, would have an easy, simple way to extend their service permission, according to need. For example, a “self service” solution for allowing developers to grant permissions for services to access non-sensitive databases makes sense. In such an environment, you can also look at sensitive permissions (say for a database holding PII data), and require a further control for granting permissions to them (for example, an OK from the data owner).

3. Embrace The Principle Of Defense In Depth (DiD)

As beautifully put by a colleague, security is like an onion — it’s made of many layers built on top of layers, and it can make you cry. In other words, when building a secure system, you need to account for different types of risk and threats, and subsequently you need to build different types of protections on top of others.

Again, let’s look at a simple example of a login system. The first security gateway you can think of in this context is the “user-password” combination. But as we all know, passwords can leak, so one should always add a second layer of defense: two-factor authentication (2FA), also known as multi-factor authentication (MFA). Wix encourages users to enable this feature for their account security. And by now, MFA is pretty standard — but is it enough? Can we assume that someone who successfully logged into the system is now trusted?

Unfortunately, not always. We looked until now at one type of attack (password stealing), and we provided another layer to protect against it, but there are certainly other attacks. For example, if we don’t protect ourselves, a Cross Site Scripting (XSS) attack can be used to hijack a user’s sessions (for example by stealing the cookies), which is as good as a login bypass. So we need to consider added layers of defense: cookie flags to prevent JS access (HTTP only), session timeouts, binding a session to a device, etc. And of course, we need to make sure we don’t expose XSS issues.

You can look at this concept in another way. When writing a feature, you should almost protect it “from scratch”, thinking all defenses might have been broken. That doesnt mean writing every line of code again, it just means being aware that certain assumptions cannot be made. For example, you can’t assume that just because your service does not have an externally reachable endpoint, it has never been accessed by malicious entities. An attacker exploiting Server-Side Request Forgery (SSRF) issues can hit your endpoint any minute. Is it protected against such issues?

At Wix, we assume a “breach mindset” at all times, meaning each developer assumes the controls leading up to the application they’re working on have already been breached. That means checking permissions, input validations and even logic — we never assume previous services are sensible.

4. Minimize Attack Surface Area

What’s the safest way to secure a server? Disconnect it from the electricity socket. Jokes aside, while we don’t want to turn our services off just to ensure they’re not abused, we certainly don’t want to leave them on if they serve no real function. If something is not needed or being used, it should not be online.

The most straightforward way to understand this concept is by looking at non-production environments (QA, staging, etc). While such environments are often needed internally during the development process, they have no business being exposed such that external users can access them. Being publicly available means they can serve as a target for an attack, as they are not “production ready” services (after all, they are in the testing phase). The probability for them to become vulnerable increases.

But this concept doesn’t apply only to whole environments. If your code contains unused or unnecessary methods, remove them before pushing to production. Otherwise, they become pains instead of assets.

5. Fail Securely

If something fails, it should do so securely. If that’s confusing, you’re not alone. Many developers overlook this principle or misunderstand it. Imagining every possible edge case on which your logic can fail is almost impossible, but it is something you need to plan for, and more often than not it’s another question of adopting the right mindset. If you assume there will be failures, then you’re more likely to include all possibilities.

For instance, a security check should have two possible outcomes: allow or deny. The credentials inputted are either correct, or they’re not. But what if the check fails entirely, say, because of an unexpected outage of electricity in the database server? Your code keeps running, but you get a “DB not found” error. Did you consider that?

In this particular instance, the answer is probably “yes”, you thought of it, either because your framework forced you to consider it (such as Java’s “checked exceptions”) or simply because it actually happens often enough that your code failed in the past. But what if it is something more subtle? What if, for example, your SQL query fails due to non-unicode characters that suddenly appeared as input? What if your S3 bucket suddenly had its permissions changed and now you can’t read from it anymore? What if the DNS server you’re using is down and suddenly instead of an NPM repo you’re hitting a compromised host?

These examples might seem ludacris to you, and it would be even more ludacris to expect you to write code to handle them. What you should do, however, is expect things to behave in an expected manner, and make sure if such things occur, you “fail securely”, like by just returning an error and stopping the execution flow.

It would make no sense to continue the login flow if the DB server is down, and it will make no sense to continue the media processing if you can’t store that image on that bucket. Break the flow, log the error, alert to the relevant channel — but don’t drop your security controls in the process.

6. Manage Your Third-Party Risk

Most modern applications use third-party services and/or import third-party code to enhance their offering. But how can we ensure secure integrations with third parties? We think about this principle a lot at Wix, as we offer third-party integrations to our user sites in many ways. For example, users can install apps from our App Market or add third-party software to their websites using our full-stack development platform called Velo.

Third-party code can be infiltrated, just like your own, but has the added complication that you have no control over it. MPM node libraries, for instance, are some of the most used in the world. But recently a few well-known cases involved them being compromised, leaving every site that used them exposed.

The most important thing is to be aware that this might happen. Keep track of all your open-source code in a software bill of materials (SBOM), and create processes for regularly reviewing it. If you can, run regular checks of all your third-party suppliers’ security practices. For example, at Wix we run a strict Third-Party Risk Management Program (TPRM) to vet third parties and assess security while working with them.

7. Remember Separation Of Duties (SoD)

Separation of duties really boils down to making sure tasks are split into (and limited to) appropriate user types, though this principle could also apply to subsystems.

The administrator of an eCommerce site, for example, should not be able to make purchases. And a user of the same site should not be promoted to administrator, as this might allow them to alter orders or give themselves free products.

The thinking behind this principle is simply that if one person is compromised or acting fraudulently, their actions shouldn’t compromise the whole environment.

8. Avoid Security By Obscurity

If you write a backdoor, it will be found. If you hard-code secrets in your code, they will be exposed. It’s not a question of “if”, but “when” — there is no way to keep things hidden forever. Hackers spend time and effort on building reconnaissance tools to target exactly these types of vulnerabilities (many such tools can be found with a quick Google search), and more often than not when you point at a target, you get a result.

The bottom line is simple: you cannot rely on hidden features to remain hidden. Instead, there should be enough security controls in place to keep your application safe when these features are found.

For example, it is common to generate access links based on randomly generated UUIDs. Consider a scenario where an anonymous user makes a purchase on your store, and you want to serve the invoice online. You cannot protect the invoice with permissions, as the user is anonymous, but it is sensitive data. So you would generate a “secret” UUID, build it into the link, and treat the “knowledge” of the link as “proof” of identity ownership.

But how long can this assumption remain true? Over time, such links (with UUID in them) might get indexed by search engines. They might end up on the Wayback Machine. They might be collected by a third-party service running on the end user’s browser (say a BI extension of some sort), then collected into some online DB, and one day accessed by a third party.

Adding a short time limit to such links (based on UUIDs) is a good compromise. We don’t rely on the link staying secret for long (so there’s no security by obscurity), just for a few hours. When the link gets discovered, it’s already no longer valid.

9. Keep Security Simple

Also known as KISS, or keep it simple, stupid. As developers, we need to keep users in mind at all times. If a service is too complicated to use, then its users might not know how to use it, and bypass it or use it incorrectly.

Take 2FA for example. We all know it’s more secure, but the process also involves a degree of manual setup. Making it as simple as possible to follow means more users will follow it, and not compromise their own accounts with weaker protections.

Adding new security functionality always makes a system more complex, so it can have an unintended negative impact on security. So keep it simple. Always weigh the value of new functionality against its complexity, and keep security architecture as simple as possible.

10. Fix Security Issues, Then Check Your Work

Thoroughly fixing security issues is important for all aspects of a business. At Wix, for example, we partner with ethical hackers through our Bug Bounty Program to help us find issues and vulnerabilities in our system, and practice fixing them. We also employ internal security and penetration testing, and the security team is constantly reviewing the production services, looking for potential bugs.

But fixing a bug is just the start. You also need to understand the vulnerability thoroughly before you fix it, and often get whoever spotted it to check your fix too. And then, when a bug is fixed, carry out regression tests to make sure it’s not reintroduced by code rollbacks. This process is crucial to make sure you’re actually advancing your application security posture.

Conclusion

By implementing security by design at Wix, we were able to build a robust and secure platform — and we hope that sharing our approach will help you do the same. We applied these principles not just to security features, but to all components of our system. We recommend considering this, whether you build from scratch or choose to rely on a secure platform like ours.

More importantly, following security by design instilled a security mindset into our company as a whole, from developers to marketing and sales. Cybersecurity should be top priority in everyone’s minds, as attacks increase and hackers find new ways of accessing sensitive information.

Taking a defensive position right from the start will put you at an advantage. Because when thinking about cybersecurity, it’s not if a breach happens. It’s when.

  • For more information on security by design, visit the Open Web Application Security Project. This non-profit community is dedicated to securing the web, and produces a range of free open-source tools, training and other resources to help improve software security.
  • To learn more about secure practices at Wix, check out wix.com/trust-center/security.

Thursday, October 29, 2020

Delightful UI Animations With Shared Element Transitions API

 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:

// Select static & shared page elements.
const overlayWrapper = document.getElementById("js-overlay");
const overlayContent = document.getElementById("js-overlay-target");

function toggleImageView(index) {
    const image = document.getElementById(`js-gallery-image-${index}`);

    // Store image parent element.
    const imageParentElement = image.parentElement;

    // Move image node from grid to modal.
    moveImageToModal(image);
  
   // Create a click listener on the overlay for the active image element.
    overlayWrapper.onclick = function () {
        // Return the image to its parent element.
         moveImageToGrid(imageParentElement);
    };
}

// Helper functions for moving the image around and toggling the overlay.

function moveImageToModal(image) {
    // Show the overlay.
    overlayWrapper.classList.add("overlay--active");

    overlayContent.append(image);
}

function moveImageToGrid(imageParentElement) {
    imageParentElement.append(overlayContent.querySelector("img"));
  
    // Hide the overlay.
    overlayWrapper.classList.remove("overlay--active");
}

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:

// This function is now asynchronous.
async function toggleImageView(index) {
  const image = document.getElementById(`js-gallery-image-${index}`);
  const imageParentElement = image.parentElement;

  // Initialize transition from the API.
  const moveTransition = document.createDocumentTransition();

  // moveImageToTarget function is now called by the API "start" function.
  await moveTransition.start(() => moveImageToModal(image));

  // Create a click listener on the overlay for the active image element.
  overlayWrapper.onclick = async function () {
    // Initialize transition from the API.
    const moveTransition = document.createDocumentTransition();

    // moveImageToContainer function is now called by the API "start" function.
    await moveTransition.start(() => moveImageToGrid(imageParentElement));
  };
}

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.

.gallery__image--active {
  page-transition-tag: active-image;
}

.gallery__image {
  contain: paint;
}

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:

async function toggleImageView(index) {
   const image = document.getElementById(`js-gallery-image-${index}`);

  // Apply a CSS class that contains the page-transition-tag before animation starts.
  image.classList.add("gallery__image--active");

  const imageParentElement = image.parentElement;

  const moveTransition = document.createDocumentTransition();
  await moveTransition.start(() => moveImageToModal(image));

  overlayWrapper.onclick = async function () {
    const moveTransition = document.createDocumentTransition();
    await moveTransition.start(() => moveImageToGrid(imageParentElement));
    
    // Remove the class which contains the page-transition-tag after the animation ends.
    image.classList.remove("gallery__image--active");
  };
}

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:

// Applies page-transition-tag to the image.
image.style.pageTransitionTag = "active-image";

// Removes page-transition-tag from the image.
image.style.pageTransitionTag = "none";

And that’s pretty much it! Let’s take a look at our example with the shared element applied:

See the Pen Image gallery - crossfade + shared element (3) [forked] by Adrian Bece.

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:

::page-transition
└─ ::page-transition-container(root)
   └─ ::page-transition-image-wrapper(root)
      ├─ ::page-transition-outgoing-image(root)
      └─ ::page-transition-incoming-image(root)

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:

::page-transition
├─ ::page-transition-container(root)└─ ::page-transition-image-wrapper(root)├─ ::page-transition-outgoing-image(root)└─ ::page-transition-incoming-image(root)
└─ ::page-transition-container(active-image)
   └─ ::page-transition-image-wrapper(active-image)
      ├─ ::page-transition-outgoing-image(active-image)
      └─ ::page-transition-incoming-image(active-image)

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:

::page-transition-container(*) {
  animation-duration: 400ms;
  animation-timing-function: ease-in-out;
}

::page-transition-container(active-image) {
  animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
See the Pen Image gallery - custom animation (4) [forked] by Adrian Bece.

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:

const isSupported = "createDocumentTransition" in document;

if(isSupported) {
    /* Shared element transitions API is supported */
} else {
    /* Shared element transitions API is not supported */
}

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:

@supports (page-transition-tag: none) {
    /* Shared element transitions API is supported */
    /* Use the Shared Element Transisitons API styles */
}

@supports not (page-transition-tag: none) {
    /* Shared element transitions API is not supported */
    /* Use a simple CSS animation if possible */
}

If we need to support even older browsers, we can conditionally apply the CSS class using JavaScript:

if("createDocumentTransition" in document) {
  document.documentElement.classList.add("shared-elements-api");
}
html.shared-elements-api {
    /* Shared element transitions API is supported */
}

html:not(.shared-elements-api) {
    /* Shared element transitions API is not supported */
}

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:

@media (prefers-reduced-motion) {
    /* Turn off all animations */
    ::page-transition-container(*),
    ::page-transition-outgoing-image(*),
    ::page-transition-incoming-image(*) {
        animation: none !important;
    }

    /* Or, better yet, create accessible alternatives for these animations  */
}

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”).

async function moveCard(isDone) {
  // Select the active card. 
  const card = this.window.event.target.closest("li");

  // Get the target list id.
  const destination = document.getElementById(
    `js-list-${isDone ? "done" : "not-done"}`
  );

  //We'll use this class to hide the item controls while the animation is running.
  card.classList.add("card-moving");

  if (document.createDocumentTransition) {
    const moveTransition = document.createDocumentTransition();

    // Run animation.
    await moveTransition.start(() => destination.appendChild(card));
  } else {
    // Fallback.
    destination.appendChild(card);
  }
}

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.
// Assign unique page-transition-tag values to all task cards.
// We could have also done this manually in CSS by targeting :nth-child().
const allCards = document.querySelectorAll(".col:not(.col-complete) li");
allCards.forEach(
    (c, index) => (c.style.pageTransitionTag = `card-${index + 1}`)
);

async function moveCard(isDone) {
  const card = this.window.event.target.closest("li");
  const destination = document.getElementById(
    `js-list-${isDone ? "done" : "not-done"}`
  );

  //We'll use this class to hide the item controls while the animation is running.
  card.classList.add("card-moving");

  if (document.createDocumentTransition) {
    // Replace the item tag with an active (moving) element tag.
    card.style.pageTransitionTag = "card-active";

    const moveTransition = document.createDocumentTransition();
    await moveTransition.start(() => destination.appendChild(card));

    // Remove the tag after the animation ends.
    card.style.pageTransitionTag = "none";
  } else {
    destination.appendChild(card);
  }
}

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.

li {
  contain: paint;
}

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.

See the Pen To-do list - with shared element (2) [forked] by Adrian Bece.

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:

/* We are applying contain property on all browsers (regardless of property support) to avoid differences in rendering and introducing bugs */
li {
  contain: paint;
}

@supports (page-transition-tag: none) {
  ::page-transition-container(card-active) {
    animation-duration: 0.3s;
  }

  ::page-transition-image-wrapper(card-active) {
    animation: popIn 0.3s cubic-bezier(0.64, 0.57, 0.67, 2) 0.1s;
  }
}

@keyframes popIn {
  from {
    transform: scale(1.3);
  }
  to {
    transform: scale(1);
  }
}

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.

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:

<section class="gallery">
  <img src="..." id="js-gallery" decoding="sync" loading="eager" />
  <aside class="gallery__controls">
    <div class="gallery__status">
      <span id="js-gallery-index">1</span> / 11
    </div>
    <button onclick="previousImage()" class="gallery__button">
      <!-- ... -->
    </button>
    <button onclick="nextImage()" class="gallery__button">
      <!-- ... -->
    </button>
  </aside>
</section>

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:

// Let's store all images in an array.
const images = [ "https://path.to/image-1.jpg", "https://path.to/image-2.jpg", "..."];
let index = 0;

function previousImage() {
  index -= 1;

  if (index < 0) {
    index = images.length - 1;
  }

  updateIndex();
  crossfadeElements();
}

function nextImage() {
  index += 1;

  if (index >= images.length) {
    index = 0;
  }

  updateIndex();
  crossfadeElements();
}

// Util functions for animation, index update and image src update.

async function crossfadeElements() {
  if (document.createDocumentTransition) {
    const imageTransition = document.createDocumentTransition();
    await imageTransition.start(updateImage);
  } else {
    updateImage();
  }
}

function updateIndex() {
  const galleryIndex = document.getElementById("js-gallery-index");
  galleryIndex.textContent = index + 1;
}

function updateImage() {
  const gallery = document.getElementById("js-gallery");
  gallery.src = images[index];
}

We’ll start with default crossfade animation, so we can focus more on the CSS and customizing animation properties.

See the Pen Crossfade image carousel - setup (1) [forked] by Adrian Bece.

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 from 1 to 0;
  • fadeIn: the next image will become less blurry and decrease in brightness as its opacity value animates from 0 to 1.
@keyframes fadeOut {
    from {
        filter: blur(0px) brightness(1) opacity(1);
    }
    to {
        filter: blur(6px) brightness(8) opacity(0);
    }
}

@keyframes fadeIn {
    from {
        filter: blur(6px) brightness(8) opacity(0);
    }
    to {
        filter: blur(0px) brightness(1) opacity(1);
    }
}

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:

/* We are applying contain property on all browsers (regardless of property support) to avoid differences in rendering and introducing bugs */
.gallery img {
    contain: paint;
}

@supports (page-transition-tag: supports-tag) {
    .gallery img {
        page-transition-tag: gallery-image;
    }

    ::page-transition-outgoing-image(gallery-image) {
        animation: fadeOut 0.4s ease-in both;
    }

    ::page-transition-incoming-image(gallery-image) {
        animation: fadeIn 0.4s ease-out 0.15s both;
    }
}

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:

// Select static page elements and initialize the counter.
const counterElement = document.getElementById("js-shopping-bag-counter");
let counter = 0;

async function addToCart() {
    const dot = createCartDot();
    const parent = this.window.event.target.closest("button");

    // Set the dot element to its starting position in a button.
    parent.append(dot);

    // Dot transition.
    if (document.createDocumentTransition) {
        const moveTransition = document.createDocumentTransition();
        // Move the dot to the cart icon.
        await moveTransition.start(() => moveDotToTarget(dot));
    }

    // Remove the dot after the animation completes.
    dot.remove();

    // Counter transition.
    if (document.createDocumentTransition) {
        const counterTransition = document.createDocumentTransition();
        await counterTransition.start(() => incrementCounter(counterElement));
    } else {
        incrementCounter();
    }
}

// Util functions for creating the dot element, moving the dot to target, and incrementing the counter.

function moveDotToTarget(dot) {
    const target = document.getElementById("js-shopping-bag-target");
    target.append(dot);
}

function incrementCounter() {
    counter += 1;
    counterElement.innerText = counter;
}

function createCartDot() {
    const dot = document.createElement("div");
    dot.classList.add("product__dot");
    return dot;
}

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:

async function addToCart() {
   /* ... */

  if (document.createDocumentTransition) {
    const moveTransition = document.createDocumentTransition();
    await moveTransition.start(() => moveDotToTarget(dot));
    dot.style.pageTransitionTag = "none";
  }

  dot.remove();

  if (document.createDocumentTransition) {
    counterElement.style.pageTransitionTag = "cart-counter";
    const counterTransition = document.createDocumentTransition();
    await counterTransition.start(() => incrementCounter(counterElement));
    counterElement.style.pageTransitionTag = "none";
  } else {
    incrementCounter();
  }
}

/* ... */

function createCartDot() {
  const dot = document.createElement("div");
  dot.classList.add("product__dot");
  dot.style.pageTransitionTag = "cart-dot";

  return dot;
}

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!

/* We are applying contain property on all browsers (regardless of property support) to avoid differences in rendering and introducing bugs */
.product__dot {
  contain: paint;
}

.shopping-bag__counter span {
  contain: paint;
}

@supports (page-transition-tag: supports-tag) {
  ::page-transition-container(cart-dot) {
    animation-duration: 0.7s;
    animation-timing-function: ease-in;
  }

  ::page-transition-outgoing-image(cart-counter) {
    animation: toDown 0.3s cubic-bezier(0.4, 0, 1, 1) both;
  }

  ::page-transition-incoming-image(cart-counter) {
    animation: fromUp 0.3s cubic-bezier(0, 0, 0.2, 1) 0.3s both;
  }
}

@keyframes toDown {
  from {
    transform: translateY(0);
    opacity: 1;
  }
  to {
    transform: translateY(4px);
    opacity: 0;
  }
}

@keyframes fromUp {
  from {
    transform: translateY(-3px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

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.

References