You spin me right round
With transform
and its arsenal of functions, you have the Swiss Army Knife of CSS animation in your toolbox: so there’s nothing you can’t do.
Except make something swing…
or scale from the top down.
The sticking point with transform
is that, by default, all of its operations originate from the center of the element, so whenever you scale something, you’re scaling it outward:
And whenever you rotate something, you’re spinning it:
And sometimes that’s fine. But other times, that just won’t do.
Thankfully, CSS also gives you the transform-origin property. This allows you to move that anchor point wherever you’d like, and have your transformations originate from there.
That means you can move the origin to the top and scale an object downwards:
Or move it to the corner and allow it to swing:
In this chapter, we’ll be doing just that as you learn the ins and outs of the transform-origin
property.
Moving the epicenter
To change where a transformation originates from, you, of course, need a transform
to work with. Let’s create a loading bar with an empty frame that fills as data loads: It won’t actually be monitoring the loading progress, but rather serve as a transition between two blocks of content.
<div class="container">
<div class="btn">Load Something!!!</div>
<div class="progress">
<div class="progress__bar"></div>
</div>
</div>
We’ve styled the frame with border and a bit of padding, and the bar with background color, which renders like so:
Now, let’s go in and add the transform
property to the bar and set the X-scale to 0:
.progress {
&__bar {
transform: scaleX(0);
}
}
Now we have an empty progress bar!
To fill it, let’s use the :active
pseudo-selector and the sibling combinator to scale the bar back to 100% width when we click the load button:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform: scaleX(0);
}
}
Now when we click on the button, the loading bar fills immediately.
To fill the bar in over time, let’s add a transition
, animating it over the course of two seconds:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform: scaleX(0);
transition: transform 1000ms;
}
}
Let’s see how the loading bar is behaving now:
We’re getting there! The bar is filling in the meter, but it’s doing it from the center, and we don’t want that. Progress bars typically load in from the left edge, filling the meter rightward. To remedy the middle-out progress bar, let’s use the transform-origin
property.
transform-origin
lets you move the epicenter of your transformation away from the default center of the element based on the values that you assign it.
The process of moving the origin is just like performing a translate: you can move it via real units, like pixels, or by a percentage of the element’s dimensions. Whatever method you choose, the X-axis is measured from the left edge of the element, while the Y-axis is measured from the top edge.
So, the default centered state of transform-origin can be written as: transform-origin: 50% 50%;
We want to move the origin of our progress bar to its left edge so that it grows in from the side, so let’s set the X-origin to 0%:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform-origin: 0% 50%;
transform: scaleX(0);
transition: transform 1000ms;
}
}
Now, when we animate the scale of the X-axis of the bar from 0% to 100%, it should start as a sliver on the left and grow to the right until it fills the frame:
Perfect! Let’s give it a finishing touch by adding a cubic-bezier()
timing function. We’d like the bar to start out without too much of an ease-in, but ease-out nicely at the end. Maybe something with a profile like this:
The values for our acceleration curve work out to cubic-bezier(.32, 0, .07, 1)
. Let’s plug them into the transition’s timing function:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform-origin: 0% 50%;
transform: scaleX(0);
transition: transform 1000ms cubic-bezier(.32,0,.07,1);
}
}
And take it for a spin:
Alright! Progress bar complete!
Let's get explicit
When it comes to setting the values of transform-origin
, you aren’t limited to lengths or percentages. You can also use CSS keywords to define your anchor point, such as left
to set the origin to the left edge of the element, or right
to put it at the far right edge:
.box--left-origin {
// set the origin to the left edge
transform-origin: left 50%;
}
.box--right-origin {
// set the origin to the left edge
transform-origin: right 50%;
}
And you can use top
and bottom
to set it at the top or bottom edges, respectively:
.box--top-origin {
// set the origin to the top edge
transform-origin: 50% top;
}
.box--bottom-origin {
// set the origin to the bottom edge
transform-origin: 50% bottom;
}
The fifth and final keyword is center
, and it can be assigned to either the X- or Y-axis, and (you guessed it!) is the same as assigning a value of 50%:
.box--left-origin {
// set the origin to the center of the element
transform-origin: center center;
}
Up until now, we’ve always assigned two values to transform-origin
, one for the X-value and one for the Y. But you can also assign it a single value instead. If that value is a number, it will apply it to the X-axis and leave the Y-value at the default value of 50%.
Since the transform-origin
for our progress bar is only concerned with moving the origin to the left, the 50% value for the Y-axis value is unnecessary. Let’s remove its Y-value, which will help make the code more explicit and indicate that we are only worried about the X-axis:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform-origin: 0%;
transform: scaleX(0);
transition: transform 1000ms cubic-bezier(.32,0,.07,1);
}
}
Now our code is cleaner, and still performs exactly the same in the browser:
But if we assign a single keyword instead to transform-origin
, rather than a numerical value, the browser will figure out which axis to apply it to, leaving the other axis at the default.
Using the left
keyword is even more explicit than simply stating 0%
for a transform-origin
value; it instantly tells you that you've moved the transform-origin
to the left edge. Let’s go back to our progress-bar one last time and replace the transform-origin
’s value with the left
keyword to make our code as clear and concise as possible:
.btn {
&:active {
& + .progress {
& > .progress__bar {
transform: scaleX(1);
}
}
}
}
.progress {
&__bar {
transform-origin: left;
transform: scaleX(0);
transition: transform 1000ms cubic-bezier(.32,0,.07,1);
}
}
When two dimensions aren't enough
You can use the transform
property’s functions to manipulate elements in either two dimensions or three. Setting up transform-origin
with X and Y values works great for 2D transformations, but what about changing the origin for 3D transformations?
You guessed it! 😎 You add a third value to the list. You can assign the X and Y values just the same as if it were a 2D origin, using percent or keywords, but the Z-axis MUST be defined using real units, such as pixels, CM, etc.
.btn {
perspective: 500px;
&:active {
& > .btn__flip {
transform: rotateX(-90deg);
}
}
&__flip {
transform-style: preserve-3d;
transform-origin: center bottom 7.5vw;
transition: transform 500ms cubic-bezier(.7, 0, .23, 1);
&--off {
transform: rotateX(0deg) translateZ(7.5vw);
}
&--on {
transform: rotateX(90deg) translateZ(7.5vw);
}
}
}
You can combine 3D transform functions with 3D transform-origin values to create three dimensional objects, such as a multi-faceted buttons!
Let's recap!
transform-origin
lets you re-position the anchor point from the default center of the element.You can set the origin using real units like
px
orrem
.You can also use percentages for X and Y.
Or use keywords:
left
andright
for X-axis,top
andbottom
for the Y-axis, andcenter
for either/or.When changing the origin in 3D space, the Z-value must be a real unit (not percent)!