Transforms

🔄 Chapter 9 — Transforms

CSS transforms move, rotate, resize, and distort elements visually — without affecting layout flow. A translated element still occupies its original space in the document; nothing around it shifts. This makes transforms the right tool for hover effects, entrance animations, card flips, parallax, and any visual motion where you don't want to disturb surrounding content.

1 — What Transforms Do

The transform property applies one or more transform functions to an element. These functions change how the element is painted to the screen — but the element still occupies its original position in the layout flow. Think of it as moving a sticker on top of an already-laid-out page.

Transforms vs. layout properties. Setting margin-left: 50px shifts an element and pushes its siblings. Setting transform: translateX(50px) shifts the element visually but leaves a "ghost" in its original position — siblings and parent containers are completely unaffected. This is why transforms are used for animation: changing transform never triggers layout recalculation.
🎨 CSS — the transform property
/* Single function */ .card { transform: rotate(45deg); } /* Multiple functions (applied right-to-left — see Section 6) */ .card { transform: translateY(-10px) scale(1.05) rotate(2deg); } /* Remove a transform */ .card { transform: none; } /* The transform creates a new stacking context — the element becomes a containing block for position:fixed children, and is composited independently by the GPU. */

2 — translate()

translate(x, y) moves an element along the X and Y axes. Both arguments accept any CSS length or percentage — percentages are relative to the element's own size, not the parent's, which enables the classic centering trick.

🎨 CSS — translate variants
/* Two-axis shorthand */ .box { transform: translate(100px, -50px); } /* right 100px, up 50px */ /* Single-axis helpers */ .box { transform: translateX(2rem); } .box { transform: translateY(-1em); } .box { transform: translateZ(50px); } /* Z-axis — needs perspective */ /* Percentage = % of the element's own size */ .tooltip { transform: translateX(-50%); } /* shift left by half its own width */ /* The centering trick — no need to know the element's size */ .centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* top/left move the top-left corner to centre, */ /* translate(-50%,-50%) then shifts back by half its own size */ }

3 — rotate()

rotate(angle) spins an element clockwise for positive angles and counter-clockwise for negative ones. It accepts deg, rad, grad, and turn units.

🎨 CSS — rotate variants
/* Angle units — all equivalent to 90° clockwise */ rotate(90deg) /* degrees */ rotate(1.5708rad) /* radians */ rotate(0.25turn) /* turns (0–1) */ rotate(100grad) /* gradians (0–400)*/ /* 3D rotation variants (require perspective for visual depth) */ rotateX(45deg) /* tilt towards/away from viewer — horizontal axis */ rotateY(45deg) /* spin left/right — vertical axis */ rotateZ(45deg) /* same as 2D rotate() */ /* Hover rotation effect */ .icon { transition: transform 0.3s ease; } .icon:hover { transform: rotate(180deg); } /* Collapsed/expanded indicator (common accordion arrow) */ .arrow { transform: rotate(0deg); transition: transform 0.25s; } .open .arrow { transform: rotate(90deg); }

4 — scale() and skew()

🎨 CSS — scale() and skew()
/* ── scale() ── */ /* Values: 1 = no change, 2 = double size, 0.5 = half size */ scale(1.5) /* uniform — same as scale(1.5, 1.5) */ scale(2, 0.5) /* double width, half height */ scaleX(-1) /* flip horizontally (mirror) */ scaleY(-1) /* flip vertically (upside down) */ /* Pulsing button effect */ .btn:active { transform: scale(0.95); } /* ── skew() ── */ /* Shears/distorts — positive X-angle tilts right, positive Y tilts down */ skew(20deg) /* X-axis skew only */ skew(15deg, 5deg) /* X and Y axis */ skewX(-30deg) /* single axis */ /* Diagonal banner / parallelogram button */ .banner { transform: skewX(-15deg); } .banner-inner { transform: skewX(15deg); /* un-skew the text inside */ }
2D Transforms Playground
box
The dashed outline shows the element's original layout position — notice how other elements are never displaced.

5 — transform-origin

transform-origin sets the pivot point around which transforms are applied. The default is 50% 50% — the centre of the element. Change it to rotate around a corner, an edge, or even a point outside the element entirely.

🎨 CSS — transform-origin values
/* Keywords */ transform-origin: center; /* default — 50% 50% */ transform-origin: top left; /* 0% 0% */ transform-origin: bottom right; /* 100% 100% */ /* Lengths or percentages */ transform-origin: 0 0; /* top-left corner */ transform-origin: 100% 0; /* top-right corner */ transform-origin: -20px 50%; /* 20px left of the element */ transform-origin: 150% 50%; /* point to the right — orbiting effect */ /* Three values include the Z-axis (for 3D transforms) */ transform-origin: 50% 50% 100px; /* Hinge effect — door swinging from its left edge */ .door { transform-origin: left center; transform: rotateY(0deg); transition: transform 0.5s ease; } .door.open { transform: rotateY(-80deg); }
transform-origin — change the pivot point
box
center (default)
The orange dot shows the transform-origin. Press Rotate to see how the origin changes the pivot.

6 — Combining Transforms — Order Matters

When multiple transform functions appear in one transform declaration, they are applied right to left. This mirrors matrix multiplication, and the order dramatically changes the result — because each function operates in the coordinate system established by all previous transforms.

translateX(50px) → rotate(45°)
A
Translate first (moves along original X axis), then rotate. The box ends up to the right of centre, then spun.
rotate(45°) → translateX(50px)
B
Rotate first (the coordinate system rotates), then translate. The "X axis" is now diagonal, so translateX moves diagonally.
🎨 CSS — transform order and multiple functions
/* Box A — translate right, then rotate */ .a { transform: translateX(50px) rotate(45deg); } /* Applied order: rotate(45°) THEN translateX(50px) → translation is along the ROTATED x-axis (diagonal direction) */ /* Box B — rotate, then translate */ .b { transform: rotate(45deg) translateX(50px); } /* Applied order: translateX(50px) THEN rotate(45°) → translation happens along original x-axis (straight right) */ /* Tip: write transforms in the ORDER you'd narrate them ("move it right, then tilt it") but remember the CSS declaration reads right-to-left in application. */ /* Common combined hover effect */ .card:hover { transform: translateY(-6px) scale(1.03) rotate(1deg); }

7 — 3D Transforms

3D transforms add depth to layouts. To see 3D perspective, you need three things: a perspective value on the parent, transform-style: preserve-3d on the container element so children exist in a shared 3D space, and 3D transform functions on the children.

🎨 CSS — the 3D setup
/* Perspective — how far the viewer is from the screen. Smaller = more dramatic. Applied to the parent. */ .scene { perspective: 600px; /* 300–800px = dramatic; 1200px+ = subtle */ perspective-origin: 50% 50%; /* viewpoint position (default) */ } /* preserve-3d — children share the scene's 3D space */ .container { transform-style: preserve-3d; } /* backface-visibility — hide an element when it faces away from viewer */ .card-face { backface-visibility: hidden; } /* 3D transform functions */ rotateX(45deg) /* tilt towards/away */ rotateY(180deg) /* spin (card flip) */ translateZ(100px) /* move towards viewer */ translate3d(x, y, z) /* three-axis move in one function */ scale3d(sx, sy, sz) /* three-axis scale */ /* perspective() as a function (applied inline on the element) */ .item { transform: perspective(600px) rotateY(45deg); } /* Use this when you want per-element perspective (no shared scene) */

The card flip pattern

3D Card Flip
Perspective:
🃏
Front Face
click to flip
Back Face
rotateY(180deg)
Click the card or the button above to flip
🎨 CSS — full card flip markup
/* HTML structure: <div class="scene"> <div class="card"> <div class="card-face card-front">Front</div> <div class="card-face card-back">Back</div> </div> </div> */ .scene { perspective: 600px; } .card { position: relative; transform-style: preserve-3d; transition: transform 0.6s ease; } .card.flipped { transform: rotateY(180deg); } .card-face { position: absolute; inset: 0; backface-visibility: hidden; /* hide when facing away */ } .card-back { transform: rotateY(180deg); /* pre-rotated so it starts facing away */ }

8 — Transforms and Transitions

Transforms become motion when combined with transition. The browser smoothly interpolates the transform values between states. Chapter 10 (Keyframe Animations) covers complex multi-step motion — here we focus on the state-to-state pattern.

🎨 CSS — transform + transition patterns
/* Hover lift + shadow */ .card { transition: transform 0.25s ease, box-shadow 0.25s ease; } .card:hover { transform: translateY(-6px) scale(1.02); box-shadow: 0 12px 32px rgba(0,0,0,0.25); } /* Icon spin on hover */ .btn .icon { transition: transform 0.4s ease; } .btn:hover .icon { transform: rotate(360deg); } /* Press / click feedback */ .btn { transition: transform 0.1s ease; } .btn:active { transform: scale(0.95); } /* Slide-in navigation panel */ .nav { transform: translateX(-100%); transition: transform 0.35s ease; } .nav.open { transform: translateX(0); } /* Staggered entrance (JS adds .visible to each in sequence) */ .item { transform: translateY(20px); opacity: 0; transition: transform 0.4s, opacity 0.4s; } .item.visible { transform: translateY(0); opacity: 1; }

9 — Performance: Why Transforms Are Fast

Transforms and opacity are the two CSS properties that the browser can animate entirely on the GPU compositor thread. Every other property that changes geometry (width, height, top, left, padding, margin…) forces a layout recalculation — the browser must re-measure every affected element before painting. Transforms skip layout and paint entirely; the compositor just moves an already-painted texture, which is essentially free.
⚠️ Slow — triggers layout
.btn:hover { width: 110%; top: -4px; left: -4px; }
✓ Fast — compositor only
.btn:hover { transform: scale(1.05); transform: translateY(-4px); }
🎨 CSS — will-change hint
/* Hint the browser to promote the element to its own compositor layer BEFORE the animation starts — eliminates the initial "jank" frame when the layer is first promoted. */ .animated-card { will-change: transform, opacity; } /* ⚠️ Overuse is harmful: every will-change element uses GPU memory. Add it only to elements you know will animate. Remove it after animation ends via JavaScript if the animation is one-shot. */ /* The three-property GPU rule — animate ONLY these for max perf: */ /* transform (position, size, rotation) */ /* opacity (fade in/out) */ /* filter (blur, brightness — varies by browser/hardware) */

10 — Individual Transform Properties Modern CSS

Modern CSS provides translate, rotate, and scale as standalone properties. They apply on top of transform and let you animate each axis independently in @keyframes or transitions — something previously impossible because changing transform in two different rules would overwrite each other.

translatetranslate: 20px 10px
rotaterotate: 45deg
scalescale: 1.2
🎨 CSS — individual transform properties
/* Old approach — hover and focus both set transform, the second declaration overwrites the first completely */ .card:hover { transform: translateY(-8px); } .card:focus { transform: scale(1.05); } /* wipes out translateY! */ /* New approach — individual properties compose independently */ .card { transition: translate 0.25s, scale 0.25s; } .card:hover { translate: 0 -8px; } /* doesn't affect scale */ .card:focus { scale: 1.05; } /* composes with translate */ /* Hover + focus together: card lifts AND scales — no conflict */ /* Syntax differences from functions */ /* translate: x y (space-separated, not comma) */ /* rotate: angle (single value; or x y z angle) */ /* scale: x (or x y) */ translate: 50px 20px; /* = translateX(50px) translateY(20px) */ rotate: 45deg; scale: 1.5; /* uniform */ scale: 1.5 1; /* X and Y */ /* Application order (always): translate → rotate → scale Regardless of declaration order in CSS */ /* Browser support: Chrome 104+, Firefox 72+, Safari 14.1+ */

11 — Quick Reference

FunctionWhat it doesUnits / notes
translate(x, y)Move elementLength or %; % = % of self
translateX(x) / translateY(y)Single-axis move
translateZ(z) / translate3d(x,y,z)Move along Z-axisNeeds perspective
rotate(angle)Spin clockwise (positive)deg, rad, turn, grad
rotateX(a) / rotateY(a) / rotateZ(a)3D rotationNeeds perspective
scale(n)Resize uniformlyUnitless ratio; 1 = no change
scale(x, y)Resize each axis-1 = flip
scaleX(n) / scaleY(n)Single-axis scale
skew(x, y)Shear/distortdeg
skewX(x) / skewY(y)Single-axis skew
PropertyFunctionNotes
transform-originSets the pivot pointDefault: 50% 50%
transform-stylepreserve-3d for 3D childrenApplied to container, not element
perspectiveDepth illusion — set on parent300–1200px typical range
backface-visibilityhidden hides back of flipped elementEssential for card flips
will-changeHint GPU layer promotionUse sparingly
translate / rotate / scaleIndividual transform propsChrome 104+, Firefox 72+, Safari 14.1+

✏️ Exercises

Each exercise can be done with just CSS — no JavaScript needed. Focus on which function creates the right visual effect, and which properties need to be on the parent vs. the element.

Exercise 1
Create a button with these three interactive states using only transform and transition: (a) on hover — lift up 4px and scale to 1.04; (b) on active (click) — press down to scale 0.97; (c) ensure the transition feels snappy on press (0.1s) but smooth on hover (0.25s). Use cubic-bezier or named easings of your choice.
Hint: :hover and :active are separate rules. :active wins over :hover when both match. Set transition on the base element so both states animate.
CSS
.btn { transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); /* spring-like easing makes the lift feel alive */ } .btn:hover { transform: translateY(-4px) scale(1.04); } .btn:active { transform: scale(0.97); transition: transform 0.1s ease; /* override transition duration on :active for a fast press snap */ }
Exercise 2
A loading spinner: create a <div class="spinner"> that is a 40×40 circle with a partial border (transparent on one side). Use a CSS animation (@keyframes spin) to rotate it 360 degrees continuously. Set transform-origin to its centre. Then write a second version (.spinner-orbit) where the spinning dot travels around a circle by combining rotate() with translateY() and an external transform-origin.
Hint: For the orbit, set transform-origin: 50% 200% (or similar) so the dot rotates around a centre point below itself. The @keyframes just goes from rotate(0deg) to rotate(360deg).
CSS
/* Standard spinner */ @keyframes spin { to { transform: rotate(360deg); } } .spinner { width: 40px; height: 40px; border-radius:50%; border: 3px solid #4f8ef7; border-top-color: transparent; animation: spin 0.8s linear infinite; /* transform-origin defaults to center — no change needed */ } /* Orbiting dot */ @keyframes orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spinner-orbit { width: 10px; height: 10px; border-radius: 50%; background: #c9a8ff; transform-origin: 50% 250%; /* orbit radius ≈ 25px below dot center */ animation: orbit 1.2s linear infinite; }
Exercise 3
Build a CSS-only tooltip that slides in from the bottom. The tooltip is a ::after pseudo-element on a .has-tooltip span. At rest: opacity: 0 and translateY(6px). On hover: opacity: 1 and translateY(0). Use transform-origin: bottom center and position the tooltip above the trigger element. Animate both opacity and transform simultaneously.
Hint: Set position: absolute; bottom: 100%; left: 50% on the pseudo-element, then use translate(-50%, 0) to centre it horizontally. The transform can contain both centering and the slide animation using multiple functions.
CSS
.has-tooltip { position: relative; display: inline-block; } .has-tooltip::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%) translateY(6px); transform-origin: bottom center; opacity: 0; transition: transform 0.2s ease, opacity 0.2s ease; white-space: nowrap; pointer-events: none; /* styling… */ background: #1e2235; color: #e2e4ec; padding: 4px 10px; border-radius: 4px; font-size: 0.8em; } .has-tooltip:hover::after { transform: translateX(-50%) translateY(0); opacity: 1; } /* HTML: <span class="has-tooltip" data-tip="Your tooltip text">hover me</span> */
Exercise 4
Build a 3D "tilt card" that reacts to mouse position using JavaScript. The card should have a perspective container, and when the mouse moves over the card, apply rotateX and rotateY transforms proportional to the cursor position within the card (max ±15 degrees). On mouse leave, smoothly animate back to no rotation. Add a subtle scale up on hover (1.03) and a glare highlight effect using a ::before pseudo-element with a radial gradient that follows the mouse.
Hint: In the mousemove handler, calculate x = (e.offsetX / card.offsetWidth - 0.5) * 30 for the Y-axis rotation and the inverse for X. On mouseleave, set transform: rotateX(0) rotateY(0). The transition on the card handles the smooth return.
CSS + JS
/* CSS */ .tilt-scene { perspective: 800px; display: inline-block; } .tilt-card { position: relative; transition: transform 0.5s ease; transform-style: preserve-3d; will-change: transform; border-radius: 12px; overflow: hidden; } /* Glare highlight */ .tilt-card::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at var(--mx,50%) var(--my,50%), rgba(255,255,255,0.15) 0%, transparent 60%); pointer-events: none; border-radius: inherit; z-index: 1; } // JavaScript const card = document.querySelector('.tilt-card'); const scene = document.querySelector('.tilt-scene'); const MAX = 15; // degrees card.addEventListener('mousemove', (e) => { card.style.transition = 'transform 0.1s ease'; const { left, top, width, height } = card.getBoundingClientRect(); const x = (e.clientX - left) / width - 0.5; // -0.5 → 0.5 const y = (e.clientY - top) / height - 0.5; card.style.transform = `scale(1.03) rotateY(${x * MAX * 2}deg) rotateX(${-y * MAX * 2}deg)`; card.style.setProperty('--mx', `${(x + 0.5) * 100}%`); card.style.setProperty('--my', `${(y + 0.5) * 100}%`); }); card.addEventListener('mouseleave', () => { card.style.transition = 'transform 0.5s ease'; card.style.transform = 'scale(1) rotateY(0deg) rotateX(0deg)'; card.style.setProperty('--mx', '50%'); card.style.setProperty('--my', '50%'); });