.webp?alt=media)
Css transform in depth
At first glance, the CSS transform property seems straightforward, as it operates in a declarative manner.
If you instruct the browser to rotate an element around Z-axis by 45deg
, scale an element by 2
, or move it along the Y-axis by 100px
, it executes these transformations effortlessly!
However, the more you work with complex CSS animations, the more you realize that there’s some intriguing logic behind CSS transforms. In the article, i aim to avoid typical mathematical explanations as I find them unnecessarily complicated.
How transform functions work
When you apply a transform property to an element, the browser creates 3D space with three axes. Refer to the following image to understand the model and the default directions of axes.
There are some transform functions available:
translateX
,translateZ
,translateY
,translate3d
- move an element along an axisrotate
,rotate3d
,rotateX
,rotateY
,rotateZ
- rotate an element along an axisskew
,skewX
,skewY
- distort the element by specific axisscale
,scaleX
,scaleY
- scale by specific axis
Every transform function is applied to transform-axes, not the element itself. The axes are positioned relative to the element according to the
transform-origin
value.
In other words, CSS transform functions are based on a 4x4 transformation matrix. Each function represents a specific matrix that is multipled by others in sequence, from left to right. However I prefer not to use that information at all.
I understand this concept might seem a bit tricky, but the following example should clarify everything.
The task: rotate the element by
45deg
with the pivot point at the center and move it along X-Axis by25px
;
As you can see the element is not moving along actual X-Axis - the translation path rotates with the element!
Another example.
The task: move the cube by
25px
and scale it by2
.
.box {
transform-origin: center;
transform: translateX(25px) scale(2);
}
But what if we change the order of the transfrom functions?
Stop and think about it for a moment.
Check the code below - everything should work as in the previous example: scale the cube by 2 and translate it by 25px
.
.box {
transform-origin: center;
transform: scale(2) translateX(25px);
}
A surprising result: the cube moves by 50px! Why does this happen? Because we are not scaling the object itself - we are scaling the axes! Before scaling, a pixel on the axis matched a viewport pixel, but after scaling, axes-pixel became twice as large.”
Again: we are not working with the element, we are working with it’s transform-axes.
Transform related properties
Transform origin
We can define where our axes will be positioned relative to an element using transform-origin
value.
To see how it affects the result of tranforms, let’s use the rotateZ
function with different transform-origin
value:
Perspective
This is a powerful feature that allows you to position your model to fell like it exists in 3D space. That property defines how far the user is from your model.
.container {
perspective: 300px;
}
.box {
transform: rotateY(50deg);
}
Transform style
Value | Description |
---|---|
flat | default value; |
preserve-3d | creates 3 dimensional axes for direct child nodes |
Perspective origin
The property defines point of view on our container with perspective property defined. MDN has amazing demo where you can fully understand the property. Anyway i don’t find the property really useful since i have never used it.
Transform functions
I am not gonna make a review of all transform function since the Internet is full of such information. Let’s pay some attention to bad-known functions.
Skew
It is quite challenging to understand this function without the knowlendge we’ve covered, but now we are ready.
If you apply skewY(45deg)
, it effectively rotates X-Axis by 45deg
. Simple as that! Points coordinates were not changed, we just changed the base axes.
Refer to the example to see how the axes change when we apply the skew
function.
Matrix3d
I think you have never heard, never used and will never use the function since it is constructed for mathematitian guys.
.box {
transform: matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
)
}
This is something called 4x4 transformation matrix ( or homogeneous matrix ). But there is a one cool thing about it - you can apply many transform functions in a short way. Here you can find a really cool explanation of what is homogeneous matrix but i suggest you just to play with some inputs on this amazing website with live demo.
Rotate3d
I know, this is not a property, but this is a functuion that requires special attention.
It allows you to define a vector around which the element will rotate. The vector starts from the transfrom-origin
.
.box {
/* transform: rotate(x, y, z, angle); */
transform: rotate3d(1, 1, 0, 45deg);
}
The feature can be actually useful for some cases. For example - interactive plane that looks at the user’s cursor.
const pythagorean = (x, y) => Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
container.addEventListener('mousemove', (e: MouseEvent) => {
const {
width,
height,
x: xOffset,
top: yOffset,
} = container.getBoundingClientRect();
const x = e.clientX - xOffset - width / 2;
const y = e.clientY - yOffset - height / 2;
const angle = (pythagorean(x, y) / pythagorean(height / 2, width / 2)) * 60;
box.style.transform = `rotate3d(${-y}, ${x}, 0, ${angle}deg)`;
shadow.style.transform = `rotate3d(${Math.abs(y)}, ${x}, 0, ${angle}deg)`;
});
If your really liked the demo you can read the explanation. But be aware, explanation is 2 times more complicated than the whole article :)
Explanation
Normalize coordinate system
First of all, i wanna get normal coordinate system, specifically i wanna move the origin of coordinate system to the center of my container. Expected result - when i move my cursor to the center, the point is (0,0), left top corner of the box is ( - width / 2, - height / 2) point.
Find rotation vector
That is good. Now i wanna get the rotation vector. The point that we got on the previous step is not the needed rotation vector. Rotation vector has to be a perpendicular to the user’s cursor vector.
[IMAGE]
I have found the vector using a math trick ( no math PHD is needed ). This formula is necessary and sufficient condition for 2 vectors to be perpendicular:
Ax * Bx + Ay * By = 0;
This is an amazing equation since we can simply consider A coords as B coords, with small changes. Bx = -Ay, By = Ax.
Ax * -Ay + Ax * Ay = 0;
Ax * Ay = Ax * Ay;
Another trick to find a perpendicular vector using the system
sqrt(x^2+y^2)=sqrt(12500)
sqrt((-50 - x)^2 + (-100 - y)^2)=sqrt(12500 + 12500)
// where -50 = currentX, -100 = currentY
// sqrt(12500) = sqrt(-50*-50 + -100*-100)
The system comes from the fact that the length of needed vector equals length of current vector and the distance between these two points can be calulated vie Pyphagor theorem.
So our rotation vector is (-y, x)
. We could use (y, -x)
but there are some limitations. Such vector will lead to rotation the element to the opposide side, direction of vector defines which direction element is gonna rotate around.
box.style.transform = `rotate3d(${-y}, ${x}, 0, 60deg)`;
In the line above i made the rotation angle fixed for simplifying understanding process. Now we are ready to make it alive.
Making rotation alive
This angle as you can see depends on how far users cursor is located from the element ( from the center of container ). How to calculate the length from one point to another point in the decarts coordinate system? Pythagoras theorem, junior school.
Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
Now my idea is somehow use the value for rotation, but now the number can be pretty large because container can be big. E.g. 300deg is not good for us.
I want the rotation to be in the next range: [0, 60]
. So i need to limit the result of my calculations with the 60 value. I need to make a projection.
x
- current value, MaxX
- maximum x value, ProjectionBase
- the value we wanna limit the current value with.
Projection = x / MaxX * ProjectionBase
x / MaxX
returns a value from 0 to 1, and then just multiply it by ProjectionBase. So if x = MaxX
then Projection
will be ProjectionBase
. That is what we need.
function pythagorean(sideA, sideB) {
return Math.sqrt(Math.pow(sideA, 2) + Math.pow(sideB, 2));
}
const angle = (pythagorean(x, y) / pythagorean(height / 2, width / 2)) * 60;
shadow.style.transform = `rotate3d(${Math.abs(y)}, ${x}, 0, ${angle}deg)`;
Small tips
Translate rotated element by real axis
The task: move rotated element by by horizontal axis.
As you remember, if we try to do something like that:
.box {
transform: rotate(60deg) translateX(50px);
}
then our element will be moved by rotated X-Axis. That does not meet the intended purpose.
We have to use some simple trigonometry. It is incredebly useful to know how to use trigonometry circle since the CSS and JS provides several helpful functions like sin
and cos
.
Let’s rotate the element. It’s axes are rotated as well, but we have to translate the element around previous axes. Fortunately, we know how our axes were changed - they were just rotaed by 60 degrees! What does that mean?
It means that every point in the current coordinate system was rotated as well. Here is the formula.
const newX = Math.cos(ROTATION_ANGLE) * prevX;
const newY = Math.sin(ROTATION_ANGE) * prevY;
So before rotation we had a point (100, 0). That would be our translation before rotation to reach needed effect. Let’s rotate the point using our formula.
const x = Math.ceil(Math.cos(ROTATION_RAD) * TRANSLATION);
const y = Math.ceil(-Math.sin(ROTATION_RAD) * TRANSLATION);
Keep in mind that i turned sin value into negative one because CSS Y coordinate is opposite to common Descartes one.
See the result!
Implicit positioned ancestor
If you apply any transfrom function to an element, it becomes a positioned ancestor, so any nested element with position: absolute
will be positioned relative to the transformed element!
Svg transform
Every svg non-meta element like rect or path can be animatied. Never forget about it!
Safari issues
If you face any flickering issues with animated elements in Safari browser just try to use the next properties on the flickering element / container.
.safari-flickering-element {
-webkit-transform-style: preserve-3d;
/* OR */
-webkit-backface-visibility: hidden;
/* OR */
-webkit-transform: translate3d(0, 0, 0);
}
Reference
Conclusion
I hope that you learned something useful from the article. If not - you are already very powerful developer! Unfortunately, a very small percentage of developers know how something actually works.
Good luck!