Emil KowalskiDesign Engineer

CSS Transforms

The transform property is the foundation of most web animations. Sonner (a toast library I built) uses CSS transforms for all its animations. Libraries like Motion rely heavily on it too. It's everywhere, so it's important to understand how it works.

This article explains what the transform property is1, and how it can be used to create great animations.

Translation

The translate() function allows us to move an element around. Positive values move it down and to the right, while negative values move up and to the left.

transform: translate(0px, 0px)
x
y

We can either use translate(x, y) or if we only want to move the element on one axis, translateX(x) or translateY(y). I usually prefer the latter because it's more readable.

 
Using translate() doesn't change an element's position in the document flow. Other elements are laid out as if it hadn’t moved at all.

transform: translate(0px, 0px)
x
y

Unlike margin or padding, percentage values in translate() are relative to the element’s own size. For example, translateY(100%) moves the element down by its own height.

transform: translate(0%, 0%)
x
y

Animations in Sonner are done exclusively using translateY() with percentages. Toasts can vary in height, so by using translateY(-100%) I know that the toast will always move up by its own height, regardless of its size.

Animation stays the same regardless of toast's height.

Vaul uses translateY() for the same reason. Since the drawer can vary in height, translateY(100%) ensures it’s fully hidden before animating in. A hardcoded value like 300px would only work if the drawer was exactly 300px tall.

I prefer using percentages for most animations, even when dimensions are hardcoded. Percentage values are less error-prone since they’re relative to the element’s own size.

A nice use case for translate() are intro animations. Here’s one I recently made at Linear, where both the cursors and the hand animate in using translateX().

Scale

scale() allows us to resize an element. It works as a multiplier: scale(2) makes the element twice as big, while scale(0.5) makes it half as big

transform: scale(1)
scale

We can also use scaleX() and scaleY() to scale an element on a single axis, or use scale(x, y) to scale both axes independently. I personally don't use it as mismatched y and x scaling values usually look awkward.

transform: scale(1, 1)
scaleX
scaleY

Unlike width and height, scaling an element also scales its children, which is a good thing. When scaling a button on click we want its font size, icons, and other content to scale too.

I used it during my work trial at Linear for the QR code button. It scales down when you click it, and text that comes in scales up slightly.

Another cool use case for scale is an iOS-like card effect. This one is made with Framer Motion (now motion), but it uses transforms under the hood.

Game of the day

A game about vikings

Are you ready? A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.

The never ending adventureIn this game set in a fairy tale world, players embark on a quest through mystical lands filled with enchanting forests and towering mountains.

A tip for using scale() is to (almost) never animate from scale(0). Nothing in the world around us can disappear and reappear in such a way. Instead, you can combine an initial scale like 0.5 with opacity animation like I did for Clerk's toast:

You almost can't notice the initial scale, but it makes the animation feel better.

Rotate

Used less often than the two previous functions, but as the name suggests, rotate() allows us to rotate an element.

transform: rotate(0deg)
rotation

The trash animation below is one example where rotate() is used. When the images animate into the trash can, they rotate to make it feel as if they are thrown into it.

3D Transforms

We can also use rotateX() and rotateY() in combination with transform-style: preserve-3d to rotate an element around a specific axis. This is useful for creating 3D effects for example. Here’s an interesting use case:

Aave's coin animation uses `rotateY()` to rotate the coin around the Y axis.

Think of rotateX() and rotateY() like screws. If you screw a piece of wood in place and then rotate the screw, the wood rotates too. Screwing it from the top and rotating is essentially like rotateY().

rotateY
rotateY

180° is the back of the element.

rotateX() works the same, but on the horizontal axis.

rotateX
rotateX

180° is the back of the element, upside down.

I recreated Aave's animation below. Inspecting it should give you an idea of how it's built. Reverse engineering things like this is a great way to learn. Be curious and inspect lots of animations.

Transform origin

Every element has an anchor point from which transform animations are executed. By default, it’s the center of the element, but we can change it to influence how the animation will behave.

transform: rotate(0deg)
transform-origin: center
rotate

A good use case for transfom-origin are origin aware animations, you can learn more about them here.

Using transform to create a toast component

You should now have a rough idea of how Sonner, the toast library, is built based on what you just read. It's essentially just different translateY() values based on the index of each toast. Heres's a simplified version of its expanded mode:

Code Playground
import "./styles.css";
import { useState, useEffect } from "react";

export default function Toaster() {
  const [toasts, setToasts] = useState(0);

  return (
    <div className="wrapper">
      <div className="toaster">
        {Array.from({ length: toasts }).map((_, i) => (
		  // Inverted index ensures correct stacking order
          <Toast key={i} index={toasts - (i + 1)} />
        ))}
      </div>
      <button
        className="button"
        onClick={() => {
          setToasts(toasts + 1);
        }}
      >
        Add toast
      </button>
    </div>
  );
}

function Toast({ index }) {
  const [mounted, setMounted] = useState(false);

  // This can be replaced by @start-style at-rule
  // once it has proper browser support.
  useEffect(() => {
    setMounted(true);
  }, []);

  return (
    <div className="toast" style={{ "--index": index }} data-mounted={mounted}>
      <span className="title">Event Created </span>
      <span className="description">Monday, January 3rd at 6:00pm</span>
    </div>
  );
}

Going deeper into animations

If you found this post useful, you should check out my animations course where we cover a lot more than just CSS Transforms. There are interactive examples, videos, interviews, and most importantly, a lot of exercises as that's how you learn the most.

Check out "Animations on the Web"
  1. 1The interactive examples in this post are inspired by Josh Comeau's post. I've tried a few different approaches, but his interactive examples work best for the purpose of this post.