CSS transforms move a box around the screen without touching the document layout. The translate, scale, rotate and skew functions can be combined into one list, they can be given a pivot point, and because they change only the rendering they are among the cheapest things to animate on a page.
<figure class="thumb">
<img src="camera.jpg" alt="A black 35mm camera with a wide lens on a wooden table">
<span class="zoom-hint">Click to enlarge</span>
<figcaption>Body and lens, sold together</figcaption>
</figure>
<div class="lightbox" role="dialog" aria-label="Enlarged photo">
<img src="camera-large.jpg" alt="A black 35mm camera with a wide lens on a wooden table">
</div>
What does a transform change, and what does it leave alone?
A transform alters how an element is painted. It does not change the space the element occupies, so its neighbours stay exactly where they were and the page does not reflow. That is the single most useful thing to know about transforms: you can move something across the screen and the rest of the layout never notices.
.thumb {
position: relative;
transition: transform 240ms ease;
}
.zoom-hint {
position: absolute;
inset-inline-end: 12px;
inset-block-end: 12px;
transform: scale(0.9);
opacity: 0;
transition: transform 240ms ease, opacity 240ms ease;
}
.thumb:hover .zoom-hint,
.thumb:focus-within .zoom-hint {
transform: scale(1);
opacity: 1;
}
Because the transform is visual, a scaled-up element can overlap its siblings, and a translated one can be clipped by an ancestor with overflow: hidden. Both are usually what you want for a zoom badge or a sliding panel, and neither is what you want if you were expecting the layout to open up. When the layout should change, animate a real layout property instead.
Applying a transform also creates a stacking context on that element, so the element and everything inside it are stacked as one unit, and it is painted in the positioned layer of its parent rather than among ordinary flow content.
What does each transform function do?
.box--move-x { transform: translateX(24px); } /* move along the horizontal axis */
.box--move-y { transform: translateY(-12px); } /* move along the vertical axis */
.box--move { transform: translate(24px, -12px); }
.box--wide { transform: scale(1.15); } /* 115% of the original size */
.box--squash { transform: scale(1.15, 0.9); } /* wider and shorter */
.box--spin { transform: rotate(12deg); } /* spin around the pivot */
.box--slant { transform: skewX(-6deg); } /* slant the sides */
Translations accept lengths and percentages, and a percentage refers to the element's own border box, so translateX(50%) moves the element by half its own width. Scale takes a unitless multiplier or a percentage. Rotation takes an angle, where positive values turn clockwise, and skew takes an angle too, tilting the shape without changing its size.
There are also three standalone properties that mirror the functions and compose with them.
.box {
translate: 0 -4px;
rotate: 6deg;
scale: 1.05;
transform: skewX(-3deg);
}
The individual properties are applied in the order translate, then rotate, then scale, and the transform list is applied after them. Using the standalone properties is convenient when a framework or a script needs to set one axis without rewriting the whole list.
How do several functions in one list combine?
The functions multiply as matrices from left to right, which means the rightmost one acts on the element's own coordinates first and the leftmost one acts in the parent's coordinates. The practical difference shows up as soon as you mix a rotation with a translation.
/* slides along the element's own (tilted) axis, then tilts */
.a { transform: rotate(45deg) translateX(40px); }
/* tilts first, then slides along the page's horizontal axis */
.b { transform: translateX(40px) rotate(45deg); }
The two do not land in the same place. The first slides the element 40 pixels along its own tilted axis and then rotates it, so it finishes diagonally away from the origin. The second rotates the element first and then slides it 40 pixels along the page's horizontal axis, so it finishes straight to the right of where it started. With several functions in the list, swapping the order is usually the quickest fix when a combination looks wrong.
How do you choose the pivot point?
Every transform happens around an origin, and the default is the centre of the element's border box. Changing it is how you make a dialog grow out of the button that opened it instead of out of its own middle.
.lightbox {
position: fixed;
inset: 0;
margin: auto;
width: min(90vw, 42rem);
height: min(70vh, 26rem);
transform: scale(0.92);
opacity: 0;
transition: transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1),
opacity 200ms ease;
}
.lightbox.is-open {
transform: scale(1);
opacity: 1;
}
.lightbox img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.lightbox--from-left { transform-origin: 0% 50%; }
The origin accepts percentages, lengths or a keyword such as top left. In the example above the panel is centred in the viewport by inset: 0 with margin: auto and a definite width and height, then scaled about its own middle. That combination gives you a dependable way to centre and animate a dialog without a flex or grid parent, and it keeps the scale pivot in the middle rather than at the edge where a percentage-based centring would push it.
How does a transform change where children are positioned?
A transformed element becomes the containing block for its positioned descendants. Any position: absolute or position: fixed child is then measured against that transformed ancestor instead of its usual reference, so a tooltip that should have been pinned to the viewport ends up pinned to a card and scrolls away with it.
.card { transform: translateZ(0); } /* .card is now the containing block */
.card__tooltip {
position: fixed;
inset-block-end: 100%;
}
The same trap catches elements with filter or backdrop-filter, which also create a containing block. The reliable fix is to keep the transform on an inner wrapper, so the element that needs to be pinned to the viewport never has a transformed ancestor.
How does a transform affect clicks?
Hit testing follows the rendered geometry rather than the layout geometry, so a scaled element is clickable across the whole area it appears to occupy. That is usually what you want for a zoomed image, but it also means a scaled panel can quietly cover a control sitting behind it.
.scrim {
position: fixed;
inset: 0;
background: rgb(15 23 42 / 0.55);
pointer-events: none; /* clicks fall through to the page behind */
}
.dialog { pointer-events: auto; }
That pairing is one way to build a modal: the backdrop covers the viewport but does not swallow clicks, while the panel itself does. Taking pointer-events: none off the backdrop is a deliberate choice, and it is the wrong choice if the page behind is meant to be inert while the dialog is open.
Notes from the field
- Transforms change painting only, so the element keeps its original space and neighbours never move.
- Percentages inside a translate refer to the element's own size, which makes responsive offsets easy.
- Function order in the list changes the outcome when you mix rotation with translation.
transform-origindefaults to the centre and decides what a scale grows out of.- Applying any transform creates a stacking context on that element.
- Text inside a scaled box is redrawn at the new size, so keep scale animations on flat shapes and images rather than on running copy.
Try it yourself: Build a product photo block for a second-hand camera shop. Put a small circular badge in the corner of the image that scales up from 0.8 with a shadow on hover, and add a full-screen lightbox that scales from 0.92 to 1 while fading in. Then change the lightbox's transform-origin to the bottom edge and reload, so the panel appears to rise out of the thumbnail rather than out of its own centre.