Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~29 min· topic 2 of 6

Topic 10.2

Transforms

In one line

The transform property moves, rotates, scales and skews an element's painted box, around a point set by transform-origin, without changing the layout around it; with perspective the same functions work in 3D.

Think of it like this

A photo stuck on a fridge with one magnet. You can slide it, spin it around the magnet, or swap it for a bigger print, and the magnet stays exactly where it was. The other photos on the fridge don't move to make room either. A transform does the same to an element: the page keeps the element's original space (the magnet's spot), and only the way the box is drawn changes.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Transform
A change to how an element is drawn (moved, turned, resized or slanted) that doesn't change the layout around it.
Transform function
One step in a transform, like translate(), rotate(), scale() or skew().
Transform origin
The fixed point a transform happens around. The centre of the box by default.
deg and turn
Units for angles: 360deg or 1turn is one full circle.
Bounding rectangle
The smallest upright rectangle that covers where the element really appears on screen.
Containing block
The box that a positioned element measures its position from.
Z axis
The direction pointing out of the screen towards you. 3D transforms can move and turn along it.
Perspective
How far your eye is from the screen in a 3D scene. Smaller distances make 3D look stronger.
Backface
The back side of an element, which you see when it is turned more than 90 degrees away from you.

Step by step

01Move a box without moving the page

Give the middle box transform: translateX(40px). It slides right, but the boxes before and after it stay exactly where they were, and the gap it left is still there. The layout was done first; the transform only changes the drawing.

style.csswhole filecss
.middle {
  transform: translateX(40px);
}

02Rotate and scale around the origin

rotate(15deg) turns the box clockwise around its centre. scale(1.2) makes it 20% bigger in every direction from its centre. Combine them in one list; a second transform declaration would replace the first, not add to it.

style.csswhole filecss
.tile {
  transform: rotate(15deg) scale(1.2);
}
/* Wrong: the second rule replaces the first. */
.tile:hover {
  transform: scale(1.4); /* rotation is lost */
}

03Change the pivot point

With transform-origin: left top, the same rotation swings the box like a door hinged at its top-left corner. Try bottom center for a sign that wobbles on its base.

style.csswhole filecss
.door {
  transform-origin: left top;
  transform: rotate(20deg);
}

04Use individual properties for independent changes

With translate, rotate and scale as separate properties, a hover can change the scale while an animation changes the rotation, and neither overwrites the other.

style.csswhole filecss
.icon {
  rotate: 10deg;
  transition: scale 0.2s;
}
.icon:hover {
  scale: 1.3; /* the 10deg rotation stays */
}

05Build a 3D flip card

The parent gets perspective. The card gets transform-style: preserve-3d so its two faces share 3D space. The back face starts turned 180deg, and both faces hide their backs. Turning the card 180deg on hover shows the back.

style.csswhole filecss
.scene { perspective: 600px; }
.card {
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.6s;
}
.scene:hover .card { transform: rotateY(180deg); }
.face { position: absolute; inset: 0; backface-visibility: hidden; }
.back { transform: rotateY(180deg); }
Build a 3D flip carddiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the matrix

    In the first example, change the transform to translate(20px, 10px) scale(2). Predict the bounding width and the matrix before running (120 and matrix(2, 0, 0, 2, 20, 10)). Does C move? (No.)

  2. 2

    Swap the order

    In the second example, change box 2 to rotate(-90deg) translate(100px). Predict where it goes (up, off the top: y becomes -80). Then try rotate(180deg) translate(100px).

  3. 3

    Make the 3D stronger

    In the flip card, change perspective: 600px to 200px, then to 2000px. Predict which one looks more dramatic (the smaller distance: your eye is closer). Then flip vertically by using rotateX(180deg) on both the hover rule and .back.

Code & diagrams

Transforms don't change layout New tab

B is drawn 90px wide and overlaps its neighbours, but its layout box is still 60px and C hasn't moved. `getComputedStyle` reports every transform as one `matrix()`: a, b, c, d hold the scale and rotation, the last two numbers the translation.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

layout size (offsetWidth): 60
drawn size (bounding rect): 90
C still starts at x = 170
transform: matrix(1.5, 0, 0, 1.5, 20, 10)
Order matters New tab

Both boxes start at (20, 20). Box 1 moves right, then spins in place. Box 2 spins first, so its own x-axis points down and the same `translate(100px)` moves it down.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

box 1 x: 120 y: 20
box 2 x: 20 y: 120
Origins and individual properties Baseline 2022+ New tab

Hover the area: all three turn 30deg, each around its own origin. Hover one tile: `scale` adds to the rotation instead of replacing it. The origin is reported in px (`top left` is `0px 0px`). The individual `rotate` property is separate from `transform`, which stays `none`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

origin: 0px 0px
rotate before hover: none
transform: none
A 3D flip card New tab

Remove `perspective` and the flip looks flat (the card only gets narrower). Remove `backface-visibility: hidden` and you see the front's mirrored text through the back.

Preview

Press Run to see the page.

Console

Nothing logged yet.
A transformed parent traps position: fixed New tab

Even `translateX(0)`, which moves nothing, makes the parent the containing block, so `top: 0; left: 0` means the parent's corner (50px down, 20px in). With `transform: none` the element is fixed to the viewport again. `filter`, `perspective`, `will-change: transform` and `contain: paint` do the same.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

with transform: top 50 left 20
without transform: top 0 left 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Two transform declarations

Write .btn { transform: translateY(-2px); } and .btn:active { transform: scale(0.95); }, expecting the pressed button to stay raised and shrink.

terminal
$ the page shows
── what you'll see ──
When pressed, the button jumps back down 2px while it shrinks.

Break #2

Transform a plain span

Write span.badge { transform: rotate(-5deg); } on a <span> inside a sentence.

terminal
$ the page shows
── what you'll see ──
Nothing happens: the badge stays straight.

Break #3

A modal that scrolls away

Put a position: fixed modal inside a wrapper that has transform: translateZ(0) (an old "performance hack") and scroll the page.

terminal
$ the page shows
── what you'll see ──
The modal sits at the wrapper's top-left corner and scrolls off the screen with the page instead of staying fixed.

Myth vs fact

Myth

transform: scale(2) makes the element take up twice the space.

Fact

Layout is unchanged; only the drawing is bigger. Neighbours don't move, and the element can overlap them. Change width if you want the layout to change.

Myth

translateZ(0) is a free performance boost.

Fact

It forces a separate compositor layer, which costs memory and can blur text, and it changes stacking and containing blocks. Use will-change deliberately and only where measuring shows a benefit (Topic 10.5).

Myth

Transform functions can be written in any order.

Fact

Each function works in the coordinate system left by the previous one, so rotate() then translate() and the reverse give different results.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Every 2D transform list is reduced to a single 3×3 matrix matrix(a, b, c, d, e, f), and 3D ones to a 4×4 matrix3d(). When animating between two lists with matching functions, the browser interpolates each function; when they don't match, it decomposes both matrices (into translate, rotate, scale, skew) and interpolates those, which can make rotations take an unexpected path. Keep function lists in the same order for predictable animation.

  • ▸

    getBoundingClientRect() accounts for transforms; offsetLeft, offsetWidth and clientWidth don't. Hit-testing (which element a click lands on) uses the transformed shape, so a scaled-up button is clickable over its whole drawn area.

  • ▸

    The perspective property on a parent gives all children one shared vanishing point; the perspective() function inside a child's own transform gives each element its own, so a row of tiles looks different. perspective() must come first in the list to affect the functions after it.

  • ▸

    transform-box (view-box, fill-box, border-box) decides what transform-origin percentages refer to. It matters mostly for SVG, where the default origin is the top-left of the SVG canvas, not the shape; transform-box: fill-box; transform-origin: center makes an SVG shape rotate around its own centre.

Remember this

  1. 1

    transform takes a list of transform functions. translate(x, y) moves the box (translateX() and translateY() move along one axis). rotate(angle) turns it; angles are written 45deg, 0.25turn (a quarter turn) or in radians 1.57rad, and positive angles turn clockwise. scale(x, y) stretches it (scale(2) doubles both ways, scale(1, 0.5) squashes it to half height, and a negative value like scale(-1, 1) mirrors it). skew(x, y) slants it like a parallelogram. Percentages in translate() are relative to the element's own size, so translate(-50%, -50%) moves a box back by half its width and height (the classic centering trick from Topic 6.5).

  2. 2

    Transforms don't affect layout. The browser first lays out the page as if there were no transform, then draws the element moved, turned or scaled. Neighbours don't shift, the parent doesn't grow, and offsetWidth still reports the untransformed size. getBoundingClientRect() is different: it returns the rectangle the transformed box actually covers on screen. This is why transforms are cheap to animate (no layout) and why a scaled-up card can overlap its neighbours.

  3. 3

    Every transform happens around the transform origin, a fixed point set by transform-origin. The default is the centre, 50% 50%, so rotate() spins around the middle and scale() grows outward from the middle. transform-origin: top left (or 0 0) makes a box rotate like a door around its top-left corner and grow towards the bottom right. The origin doesn't move the box by itself; it only changes the pivot point.

  4. 4

    Order matters. transform: translate(100px) rotate(90deg) moves right 100px, then turns in place. transform: rotate(90deg) translate(100px) turns first, so the box's own "right" now points down, and the translate moves it down 100px. Read the list from left to right as instructions to the box's own coordinate system. Modern CSS also has individual transform properties, translate, rotate and scale (Baseline 2022): they are always applied in that fixed order (translate, then rotate, then scale), then the transform list, and each can be changed or animated on its own without repeating the others.

  5. 5

    A transform (any value except none) has side effects you must know: the element becomes a stacking context (its z-index children are layered inside it, Topic 6.3), and it becomes the containing block for descendants with position: absolute and position: fixed. So a position: fixed modal inside a transformed parent is positioned against the parent, not the viewport, and scrolls with it. Transforms also don't apply to plain inline boxes like a <span> in text; give it display: inline-block first.

  6. 6

    3D transforms add a third axis, Z, pointing out of the screen towards you. rotateX() tips the box forward or back (around the horizontal axis), rotateY() turns it like a door (around the vertical axis), rotateZ() is the same as rotate(), and translateZ() moves it closer or further. To look 3D, the scene needs perspective: perspective: 600px on the parent sets how far the viewer's eye is from the screen (smaller is more dramatic), and perspective-origin sets where the eye looks from. transform-style: preserve-3d on a parent keeps its children in one shared 3D space (needed for a flip card with two faces), and backface-visibility: hidden hides a face when it's turned away from you.

Explain it without notes

01

What does transform do to an element, and how is it different from changing left or width?

02

What is transform-origin, and what is its default?

03

Why does transform: rotate(90deg) translate(100px) move the element differently from translate(100px) rotate(90deg)?

04

Which side effects does a transform have on stacking and positioning?

05

What do perspective, transform-style: preserve-3d and backface-visibility each do in a 3D flip card?

Practice

01

Make a .card that lifts 6px and grows 3% on hover, smoothly, without moving any other card.

02

Center a 120px × 80px .popup inside a position: relative parent using position: absolute and a transform.

03

Make a .needle (a thin bar) that rotates 45deg around its bottom end when its .gauge parent is hovered.

Trade-offs

  • ↔

    Transforms animate cheaply because they skip layout, but for the same reason the page doesn't make room: use them for motion and emphasis, not to change the real size of things.

  • ↔

    Individual translate/rotate/scale properties compose neatly, but their order is fixed (translate, rotate, scale); when you need another order, use the transform list.

  • ↔

    3D transforms look impressive but can cause blurry text while animating and extra GPU memory for layers; keep them for small, purposeful effects.

Done when you can

  • I can use translate, rotate, scale and skew, with the right units.

  • I know transforms don't change layout, and I can see the difference between offsetWidth and getBoundingClientRect().

  • I can change the pivot with transform-origin.

  • I can predict how the order of transform functions changes the result.

  • I know a transform creates a stacking context and traps position: fixed.

  • I can build a 3D flip card with perspective, preserve-3d and backface-visibility.