CSS transforms let you move, rotate, scale, and skew elements without affecting document flow. No other element is displaced — the transformed element occupies its original space in the layout while visually appearing elsewhere. Combined with transitions and animations, transforms are the most performance-efficient tool for motion in CSS, since they run on the GPU without triggering layout reflow. In this lesson you will master all 2D and key 3D transforms.
2D Transform Functions
| Function | Effect | Example |
|---|---|---|
translate(x, y) |
Move along x and y axes | transform: translate(20px, -10px) |
translateX(x) |
Move horizontally only | transform: translateX(50%) |
translateY(y) |
Move vertically only | transform: translateY(-8px) |
scale(x, y) |
Resize — 1 = original, 2 = double | transform: scale(1.05) |
scaleX(x) / scaleY(y) |
Resize on one axis only | transform: scaleX(0) (collapse) |
rotate(angle) |
Rotate around transform-origin | transform: rotate(45deg) |
skewX(angle) / skewY(angle) |
Shear/slant along an axis | transform: skewX(-10deg) |
matrix(a,b,c,d,e,f) |
Combine all 2D transforms in one | Advanced — used by JS animation libraries |
transform-origin
| Value | Rotation/Scale pivot point | Common Use |
|---|---|---|
center (default) |
Middle of element | Scale and rotate from centre |
top left / 0 0 |
Top-left corner | Page-curl effects, corner rotations |
left center |
Left edge, vertically centred | scaleX() expand from left — nav underlines |
bottom center |
Bottom edge, horizontally centred | Flip-up reveals, tooltip arrows |
50% 100% |
Bottom centre (same as above) | Fine-grained % control |
Key 3D Transform Functions
| Function | Effect | Required Container Property |
|---|---|---|
rotateX(angle) |
Rotate around horizontal axis — top/bottom flip | perspective on parent |
rotateY(angle) |
Rotate around vertical axis — left/right flip | perspective on parent |
rotateZ(angle) |
Rotate in the Z plane — same as 2D rotate() | No perspective needed |
translateZ(z) |
Move toward/away from viewer | perspective on parent |
perspective(n) |
Set depth inside the transform function itself | Applied to the element directly |
transform property, they are applied right to left (mathematically). The order matters: transform: rotate(45deg) translateX(100px) first rotates, then moves along the new rotated axis. transform: translateX(100px) rotate(45deg) first moves right, then rotates in place — a visually different result.transform: translateX(-50%) translateY(-50%) on an absolutely positioned element with top: 50%; left: 50% to perfectly centre it regardless of its size. This is more reliable than negative margins because it works even when the element’s dimensions are unknown — a common pattern for modals, tooltips, and overlays.perspective value on the parent container (e.g. perspective: 800px) to actually look three-dimensional. Without perspective, rotateY(45deg) renders as a flat 2D squash because there is no vanishing point defined. The perspective value controls how dramatic the depth effect appears — lower values mean more extreme perspective.Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<title>CSS Transforms</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; padding: 40px 32px; }
h3 { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 40px 0 16px; }
.row { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; }
.box {
width: 80px; height: 80px;
background: #4f46e5;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 0.65rem; font-weight: 700; color: white; text-align: center;
cursor: pointer;
transition: transform 0.35s ease;
}
/* Individual transform demos */
.demo-translate:hover { transform: translate(12px, -12px); }
.demo-scale:hover { transform: scale(1.3); }
.demo-rotate:hover { transform: rotate(45deg); }
.demo-skew:hover { transform: skewX(-15deg); }
.demo-chain:hover { transform: translateY(-8px) scale(1.1) rotate(10deg); }
/* transform-origin demos */
.origin-demo {
width: 100px; height: 60px;
background: #0891b2;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 0.65rem; font-weight: 700; color: white; text-align: center;
transition: transform 0.4s ease-in-out;
cursor: pointer;
}
.origin-center { transform-origin: center; }
.origin-center:hover { transform: rotate(180deg); }
.origin-top-left { transform-origin: top left; }
.origin-top-left:hover { transform: rotate(180deg); }
.origin-left { transform-origin: left center; }
.origin-left:hover { transform: scaleX(1.6); }
/* 3D flip card */
.flip-scene {
perspective: 800px;
width: 180px; height: 120px;
}
.flip-card {
width: 100%; height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
cursor: pointer;
}
.flip-scene:hover .flip-card { transform: rotateY(180deg); }
.flip-front, .flip-back {
position: absolute;
inset: 0;
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 0.85rem; font-weight: 700;
backface-visibility: hidden;
}
.flip-front { background: #4f46e5; color: white; }
.flip-back { background: #10b981; color: white; transform: rotateY(180deg); }
/* scaleX underline reveal */
.underline-link {
display: inline-block;
position: relative;
font-size: 1rem;
font-weight: 600;
color: #0f172a;
text-decoration: none;
padding-bottom: 3px;
}
.underline-link::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px;
background: #4f46e5;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.3s ease-out;
}
.underline-link:hover::after { transform: scaleX(1); }
</style>
</head>
<body>
<h3>2D Transforms — hover each box</h3>
<div class="row">
<div class="box demo-translate">translate</div>
<div class="box demo-scale">scale</div>
<div class="box demo-rotate">rotate</div>
<div class="box demo-skew">skewX</div>
<div class="box demo-chain" style="background:#7c3aed">chained</div>
</div>
<h3>transform-origin — same rotate(180deg), different pivot</h3>
<div class="row">
<div class="origin-demo origin-center">origin: center</div>
<div class="origin-demo origin-top-left">origin: top left</div>
<div class="origin-demo origin-left" style="background:#7c3aed">scaleX left</div>
</div>
<h3>3D Flip Card — hover to flip</h3>
<div class="row">
<div class="flip-scene">
<div class="flip-card">
<div class="flip-front">Front</div>
<div class="flip-back">Back</div>
</div>
</div>
</div>
<h3>scaleX Underline — hover the link</h3>
<a href="#" class="underline-link">Animated underline reveal</a>
</body>
</html>
How It Works
Step 1 — Transforms Do Not Affect Document Flow
transform: translate(12px, -12px) moves the element visually but its original box still occupies the same space in the layout. Adjacent elements do not shift. This is why transforms are the correct tool for hover effects — using margin-top: -12px would push other elements around.
Step 2 — Chained Transforms Apply Right to Left
transform: translateY(-8px) scale(1.1) rotate(10deg) first rotates 10 degrees in place, then scales the rotated element by 1.1, then translates the result upward. Each function operates on the coordinate system established by the previous ones — order is critical.
Step 3 — transform-origin Changes the Pivot Point
Both origin boxes rotate 180 degrees, but with different pivot points. The centre-origin box spins in place; the top-left-origin box swings like a door hinge at its top-left corner. The scaleX origin box expands from its left edge — perfect for nav underline reveals using transform-origin: left center.
Step 4 — 3D Flip Requires preserve-3d and backface-visibility
transform-style: preserve-3d on the card tells the browser that child elements (.flip-front and .flip-back) exist in the same 3D space as the card. Without it, each face would be flattened into 2D. backface-visibility: hidden hides each face when it is rotated away from the viewer — otherwise both faces are visible at once.
Step 5 — scaleX(0) to scaleX(1) Draws the Underline
The ::after pseudo-element starts at scaleX(0) — zero width visually, but still present in the DOM. transform-origin: left center makes it expand from left to right. On hover, scaleX(1) restores it to full width. The transition on transform animates the reveal smoothly.
Real-World Example: Icon Button with Rotation Feedback
/* transform-patterns.css */
/* ── Lift card on hover ── */
.lift-card {
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.lift-card:hover {
transform: translateY(-6px) scale(1.01);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
/* ── Spinning loader icon ── */
.spin-icon {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Chevron arrow that rotates open ── */
.accordion-trigger { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.accordion-chevron {
display: inline-block;
transition: transform 0.25s ease;
}
.accordion-trigger[aria-expanded="true"] .accordion-chevron {
transform: rotate(180deg);
}
/* ── Image zoom on hover ── */
.img-zoom { overflow: hidden; border-radius: 12px; }
.img-zoom img {
display: block;
width: 100%;
transition: transform 0.4s ease;
}
.img-zoom:hover img { transform: scale(1.06); }
/* ── Slide-in notification ── */
.toast {
transform: translateX(calc(100% + 24px));
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toast.visible { transform: translateX(0); }
/* ── Shake animation for form errors ── */
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-5px); }
80% { transform: translateX(5px); }
}
.input-error { animation: shake 0.5s ease; }
Common Mistakes
Mistake 1 — Forgetting transform-style: preserve-3d for 3D children
❌ Wrong — 3D child transforms are flattened into 2D without preserve-3d:
.card { transition: transform 0.6s; }
.card:hover { transform: rotateY(180deg); }
/* .flip-back is not visible — it collapses flat without preserve-3d */
✅ Correct — enable 3D context on the container:
.card { transform-style: preserve-3d; transition: transform 0.6s; }
Mistake 2 — Using position offsets instead of translate
❌ Wrong — animating top/left triggers layout recalculation each frame:
.btn { position: relative; top: 0; transition: top 0.2s ease; }
.btn:hover { top: -4px; }
✅ Correct — translateY is GPU-composited and causes no layout reflow:
.btn { transition: transform 0.2s ease; }
.btn:hover { transform: translateY(-4px); }
Mistake 3 — Missing perspective on 3D parent
❌ Wrong — rotateY without perspective just squashes the element flat:
.card:hover { transform: rotateY(45deg); } /* looks like a 2D squash */
✅ Correct — add perspective to the parent container:
.scene { perspective: 800px; }
.card:hover { transform: rotateY(45deg); } /* now has genuine 3D depth */
Quick Reference
| Function | Example | Notes |
|---|---|---|
translate(x, y) |
translate(-50%, -50%) |
Centre trick: top:50% left:50% + translate |
scale(n) |
scale(1.05) |
Subtle hover scale — stay below 1.1 for UI |
rotate(deg) |
rotate(45deg) |
Chevron open/close pattern |
scaleX(n) |
scaleX(0) → scaleX(1) |
Underline / progress bar reveal |
transform-origin |
left center |
Changes pivot for rotate and scale |
transform-style |
preserve-3d |
Required for 3D child elements |
backface-visibility |
hidden |
Hides reverse face in flip cards |