CSS Transforms

▶ Try It Yourself

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
Note: When you chain multiple transform functions in a single 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.
Tip: Use 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.
Warning: 3D transforms require a 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 */

▶ Try It Yourself

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

🧠 Test Yourself

You want an underline to expand from left to right on hover using transform: scaleX(). Which transform-origin value should you use?





▶ Try It Yourself