CSS Transforms
Move, rotate, scale, and skew elements in 2D and 3D space without affecting document flow. Transforms are GPU-accelerated and perfect for animations.
2D Transforms
2D transforms modify an element's coordinate space on the flat plane. They do not affect the layout of surrounding elements — the element keeps its original space in the document flow while visually moving.
.element { transform: translate(50px, 20px); /* move right 50px, down 20px */ transform: rotate(45deg); /* rotate 45 degrees clockwise */ transform: scale(1.5); /* scale up 150% */ transform: skew(10deg, 5deg); /* skew along X and Y */ }
translate(), translateX(), translateY()
Moves an element from its current position. Accepts length values (px, rem, %). Percentages refer to the element's own size, not its parent.
/* translate(x, y) — shorthand for both axes */ .move-both { transform: translate(100px, 50px); } /* Individual axis functions */ .move-right { transform: translateX(80px); } .move-down { transform: translateY(40px); } /* Negative values move in opposite direction */ .move-left { transform: translateX(-60px); } .move-up { transform: translateY(-30px); } /* Percentage is relative to the element itself */ .move-half { transform: translate(50%, 50%); }
(40px)
(-30px)
(30px, -20px)
When you use translate(50%, 50%), the 50% refers to 50% of the element's own width/height, not the parent. This is what makes the translate(-50%, -50%) centering trick work.
rotate()
Rotates an element clockwise around its center point (by default). Use negative values for counter-clockwise rotation. Accepts deg, rad, turn, and grad.
.rotate-45 { transform: rotate(45deg); } .rotate-neg { transform: rotate(-90deg); } .rotate-half { transform: rotate(0.5turn); } /* 180 degrees */ .rotate-full { transform: rotate(1turn); } /* 360 degrees */
scale()
Resizes an element. A value of 1 is normal size, 2 is double, 0.5 is half. You can scale each axis independently with scaleX() and scaleY().
.grow { transform: scale(1.5); } /* 150% both axes */ .shrink { transform: scale(0.75); } /* 75% both axes */ .stretch-x { transform: scaleX(2); } /* double width only */ .stretch-y { transform: scaleY(1.5); } /* 150% height only */ .flip-h { transform: scaleX(-1); } /* mirror horizontally */ .flip-v { transform: scaleY(-1); } /* mirror vertically */
Flip!
skew()
Tilts an element along the X and/or Y axis. Useful for creating parallelogram shapes and stylistic effects.
.skew-x { transform: skewX(15deg); } .skew-y { transform: skewY(10deg); } .skew-both { transform: skew(15deg, 5deg); } /* Parallelogram button trick */ .parallelogram { transform: skewX(-15deg); } .parallelogram span { display: inline-block; transform: skewX(15deg); /* un-skew the text */ }
Combining Transforms — Order Matters!
You can chain multiple transforms in a single transform property. They apply right to left (the last function is applied first). This means the order of transforms produces different results!
/* These produce DIFFERENT results! */ .order-a { transform: translate(100px, 0) rotate(45deg); /* Rotate first, then translate in the rotated coordinate system */ } .order-b { transform: rotate(45deg) translate(100px, 0); /* Translate first, then rotate around original center */ } /* Common combo: scale + translate + rotate */ .fancy-hover:hover { transform: translateY(-10px) scale(1.05) rotate(2deg); }
translate then rotate
rotate then translate
Setting transform twice on the same element replaces the first value. To combine transforms, put them all in one declaration: transform: translateX(10px) rotate(45deg) scale(1.2);
transform-origin
By default, transforms happen around the element's center (50% 50%). Change the pivot point with transform-origin.
.from-corner { transform-origin: top left; } .from-bottom { transform-origin: center bottom; } .from-right { transform-origin: right center; } .custom { transform-origin: 20% 80%; } .pixel-precise { transform-origin: 0 0; }
center center
top left
bottom right
3D Transforms
CSS can transform elements in three dimensions. 3D transforms add a Z-axis (depth) and require perspective on the parent (or the element itself) to create the illusion of depth.
perspective
The perspective property defines how far the viewer is from the Z=0 plane. Smaller values create more dramatic 3D effects. It goes on the parent element.
/* On the parent container */ .scene { perspective: 800px; /* viewer distance */ perspective-origin: center; /* vanishing point */ } /* OR as a transform function on the element itself */ .element { transform: perspective(800px) rotateY(45deg); } /* Lower perspective = more dramatic 3D */ .dramatic { perspective: 200px; } .subtle { perspective: 1500px; }
The perspective CSS property on a parent affects all children from a single viewpoint. The perspective() function in transform gives each element its own vanishing point. For most use cases, put perspective on the parent.
rotateX(), rotateY(), rotateZ()
Rotate around individual 3D axes. rotateX tilts forward/back, rotateY turns left/right, rotateZ is the same as 2D rotate().
.tilt-forward { transform: rotateX(45deg); } /* tilts toward viewer */ .turn-right { transform: rotateY(45deg); } /* turns like a door */ .spin-flat { transform: rotateZ(45deg); } /* same as rotate(45deg) */
rotateX(55deg)
rotateY(55deg)
rotateZ(55deg)
translateZ() & 3D Space
translateZ() moves an element along the depth axis (toward or away from the viewer). Requires perspective on the parent to be visible.
.scene { perspective: 600px; } .closer { transform: translateZ(100px); } /* appears larger/closer */ .farther { transform: translateZ(-100px); } /* appears smaller/farther */ /* 3D shorthand */ .move-3d { transform: translate3d(50px, 20px, 80px); }
transform-style: preserve-3d
By default, child elements are flattened into the parent's plane. Use preserve-3d to let children exist in true 3D space relative to their parent.
.card-container { perspective: 1000px; } .card { transform-style: preserve-3d; /* children keep their 3D position */ transition: transform 0.6s; } .card:hover { transform: rotateY(180deg); } .card-front, .card-back { position: absolute; backface-visibility: hidden; } .card-back { transform: rotateY(180deg); /* pre-flip the back face */ }
backface-visibility
Controls whether an element's back face is visible when facing away from the viewer. Essential for card-flip effects.
.card-face { backface-visibility: hidden; /* hide when rotated past 90deg */ } /* Default is visible */ .always-show { backface-visibility: visible; }
3D Card Flip
The classic card-flip effect combines perspective, preserve-3d, backface-visibility, and rotateY.
.flip-container { perspective: 1000px; width: 250px; height: 300px; } .flip-card { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.7s ease; } .flip-container:hover .flip-card { transform: rotateY(180deg); } .flip-front, .flip-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 16px; display: flex; align-items: center; justify-content: center; } .flip-back { transform: rotateY(180deg); }
Hover me to flip!
rotateY(180deg) applied
Centering with translate(-50%, -50%)
A classic technique to center an element both vertically and horizontally, regardless of its dimensions.
.centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* -50% of the element's OWN width and height */ }
For new projects, prefer display: grid; place-items: center; or Flexbox centering. The translate trick remains useful when you need absolute positioning (modals, tooltips, overlays).
Hover Lift Effect
One of the most popular UI micro-interactions: a card that lifts up and gains a shadow on hover, using translateY and box-shadow.
.lift-card { transition: transform 0.3s ease, box-shadow 0.3s ease; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .lift-card:hover { transform: translateY(-8px); box-shadow: 0 12px 24px rgba(0,0,0,0.2); }
Gotchas
Any element with a transform (even translateZ(0)) creates a new stacking context. This means z-index values are scoped to that context and position: fixed children will behave like position: absolute relative to the transformed element, not the viewport.
You cannot transform display: inline elements (like <span> or <a>). Change them to display: inline-block or display: block first.
Unlike margins and paddings, translate(50%, 50%) uses the element's own width and height for the percentage calculation, not the parent's dimensions.
rotate(45deg) translateX(100px) and translateX(100px) rotate(45deg) produce very different results. Transforms are applied from right to left (last listed is first applied).
Pro Tips
Adding transform: translateZ(0) or will-change: transform can promote an element to its own compositor layer, improving animation performance. But use sparingly — too many layers waste GPU memory.
Animating transform and opacity is far cheaper than animating top, left, width, or height. The former only triggers composite, while the latter triggers layout recalculation.
Modern browsers support translate, rotate, and scale as individual CSS properties, making it easier to animate them independently without overwriting each other:
/* Modern individual transform properties */ .modern { translate: 50px 20px; rotate: 45deg; scale: 1.2; /* These can be transitioned independently! */ } .modern:hover { rotate: 90deg; /* only rotation changes */ /* translate and scale remain unaffected */ }
If your 3D transform is not working, check: (1) Does the parent have perspective? (2) Is transform-style: preserve-3d set on the right element? (3) Is overflow: hidden on a parent clipping the 3D space? Overflow hidden can break preserve-3d.