Lesson 17 of 25

CSS Transform

Transforms Change the Picture, Not the Layout

transform moves, rotates, resizes or skews an element visually — and here is the crucial part: it does none of that to the layout. The space the element occupied is still reserved at its original size and position, and nothing around it shifts. The browser lays the page out first, then paints this element somewhere else.

That is exactly why transforms are the right tool for interface motion. Moving a card up four pixels on hover with transform: translateY(-4px) affects nothing else on the page, so the browser only has to repaint. Doing the same with margin-top: -4px or top: -4px forces it to recalculate the position of everything after it, on every frame of the animation. On a mid-range phone that difference is visible as stutter.

There are two consequences worth knowing up front. Because the layout is unchanged, a transformed element can overlap its neighbours — a scale(1.5) card will happily sit on top of the one beside it. And because the element is genuinely drawn in the new place, it is clickable where you see it, not where its layout box is. That is intuitive on screen but surprising if you were expecting the box model to be involved at all.

One limitation catches people out: transforms do not apply to plain inline elements. A <span> with transform: rotate(5deg) stays resolutely upright. Give it display: inline-block and it works.

Example
/* Smooth, because the layout does not change */
.card {
  transition: transform 0.2s ease-out;
}
.card:hover {
  transform: translateY(-6px);
}

/* The same movement done the expensive way */
.card:hover {
  position: relative;
  top: -6px;      /* forces layout work on every frame */
}

/* Ignored — a plain inline element cannot be transformed */
span { transform: rotate(5deg); }

/* Fixed */
span { display: inline-block; transform: rotate(5deg); }
  • The element's layout box stays where it was — neighbours never move
  • Transformed elements can overlap the things around them
  • Cheap to animate: no layout recalculation, only repainting
  • Does not apply to non-replaced inline elements such as a bare <span>
  • A transform on an element also creates a stacking context and a containing block for its positioned descendants

The Four Functions You Will Actually Use

translate(x, y) moves the element. Positive values go right and down. You can move one axis only with translateX() or translateY(). Its percentages behave unlike almost everything else in CSS: they are relative to the element's own size, not its parent's. translateX(100%) shifts the element exactly its own width to the right, which is how off-canvas drawers slide in and out cleanly at any width.

rotate() takes an angle — rotate(45deg), and negative values turn anticlockwise. scale() resizes: scale(1.1) is ten per cent larger, scale(0.9) smaller, and two arguments scale the axes separately. Remember that scaling enlarges everything inside too, including text and borders, so a heavily scaled element can look slightly soft. skew() slants the element, and it is by far the least used of the four — mostly for angled section dividers.

That percentage rule gives you the most-copied snippet in CSS. Place an element with top: 50%; left: 50% and its top-left corner sits at the centre of its anchor, leaving it visibly off-centre. Adding transform: translate(-50%, -50%) pulls it back by half its own width and height, landing it dead centre — and it works without knowing the element's size in advance.

Example
/* Move */
.a { transform: translate(40px, 12px); }
.b { transform: translateY(-6px); }
.drawer { transform: translateX(-100%); }   /* exactly its own width, off-screen */

/* Rotate */
.c { transform: rotate(45deg); }
.d { transform: rotate(-8deg); }

/* Scale */
.e { transform: scale(1.1); }
.f { transform: scale(1.4, 0.8); }   /* wider and shorter */

/* Skew — occasional, for angled dividers */
.g { transform: skewY(-3deg); }

/* The centring snippet, with no size known in advance */
.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
Notes
  • For centring inside a container you control, flexbox or place-items: center is simpler and does not need positioning at all. The translate(-50%, -50%) technique earns its place when the element must be taken out of flow — an overlay, a tooltip, a modal — where centring by layout is not available.

Order Matters, and So Does the Second Declaration

You can list several functions in one transform, separated by spaces, and they are applied one after another from left to right — each operating on the coordinate system the previous one produced. That means the order changes the result. translateX(100px) rotate(45deg) slides the element a hundred pixels to the right and then spins it in place. rotate(45deg) translateX(100px) spins first, so the element then travels a hundred pixels along its own now-tilted axis and ends up somewhere quite different. Neither is wrong; they are different instructions.

The sharper trap is that transform is a single property, so a second declaration replaces the first rather than adding to it. Give a card transform: translateY(-4px) and then a hover rule with transform: scale(1.05), and on hover the upward shift disappears — the hover value overwrote it entirely. Every combined state has to repeat the full list: transform: translateY(-4px) scale(1.05).

Modern CSS offers a neater way out. translate, rotate and scale also exist as independent properties, so they can be set and changed separately without one wiping out another. The browser applies them in a fixed order — translate, then rotate, then scale, then any transform — and they can be transitioned individually, which makes hover states with two moving parts far easier to write.

transform-origin decides the point everything pivots around. It defaults to the centre of the element, which is why rotate() spins in place. Move it to a corner with transform-origin: top left and the same rotation swings the element around that corner instead — the difference between a card tilting and a door opening.

Example
/* Different order, different result */
.a { transform: translateX(100px) rotate(45deg); }
.b { transform: rotate(45deg) translateX(100px); }

/* The overwrite trap */
.card       { transform: translateY(-4px); }
.card:hover { transform: scale(1.05); }                    /* lift is lost */
.card:hover { transform: translateY(-4px) scale(1.05); }   /* repeat it all */

/* Independent properties — no repetition, no overwriting */
.card {
  translate: 0 -4px;
  scale: 1;
  transition: scale 0.2s ease-out, translate 0.2s ease-out;
}
.card:hover { scale: 1.05; }   /* the translate is untouched */

/* Change the pivot point */
.badge  { transform-origin: top left; transform: rotate(-8deg); }
.door   { transform-origin: left center; transform: rotateY(-30deg); }
  • Functions apply left to right, each on the result of the last
  • A later transform declaration replaces the earlier one completely
  • translate, rotate and scale as separate properties avoid that entirely
  • transform-origin defaults to the centre of the element
  • Percentages inside translate() are relative to the element's own size

Adding a Third Dimension

3D transforms rotate an element around the axes that point into the screen. rotateX() tips it forwards and backwards, like a lid. rotateY() swings it like a door. rotateZ() is the same as plain rotate(). On their own these look flat and wrong, because without perspective the browser draws them as a simple squash.

perspective supplies the missing depth. It is a distance — how far the viewer is imagined to be from the screen — and smaller values mean a closer viewer and a more dramatic effect. Set it on the parent of the thing you are rotating, so several children share one consistent viewpoint. There is also a perspective() function you can put inside transform, which gives each element its own private viewpoint; that is fine for a single element and wrong for a group, because each one then appears to be viewed head-on.

The flip card is the standard exercise and it needs two more properties. transform-style: preserve-3d on the rotating element tells the browser to keep its children in the same 3D space instead of flattening them. backface-visibility: hidden hides the reverse side of each face, so that when the card turns you see the back panel rather than a mirrored copy of the front.

Use 3D sparingly. A single flip on a card that genuinely has two sides is charming; a page where several things rotate in depth is disorientating, and it is one of the effects most likely to bother someone who has asked for reduced motion.

Example
/* Perspective on the parent — one shared viewpoint */
.flip-card {
  perspective: 900px;
  width: 260px;
  height: 160px;
}

/* The element that actually turns */
.flip-card__inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.6s ease;
}

.flip-card:hover .flip-card__inner {
  transform: rotateY(180deg);
}

/* Two faces stacked on top of each other */
.flip-card__front,
.flip-card__back {
  position: absolute;
  inset: 0;                     /* top, right, bottom, left all 0 */
  backface-visibility: hidden;
  border-radius: 12px;
  display: grid;
  place-items: center;
}

.flip-card__front { background: #d1039e; color: #ffffff; }
.flip-card__back  { background: #5b026b; color: #ffffff; transform: rotateY(180deg); }
Notes
  • A transform on an element makes it the containing block for any position: fixed descendant, so a fixed header inside a transformed wrapper will scroll with the page instead of staying put. This is the same behaviour described in the positioning lesson, seen from the other side — and it is worth remembering before you add a decorative transform to a large wrapper.
Ask AI