transform-origin
Every transform — rotate(), scale(), skew() — happens around a fixed reference point on the element, and transform-origin is what sets that point. Changing it is the difference between a button that grows evenly from its center and one that grows outward from a specific corner, or a card that spins in place versus one that swings like a door on a hinge.
The Default: Dead Center
Without setting transform-origin yourself, every element transforms around its own center — equivalent to transform-origin: 50% 50% (also writable as center center, or just center).
.box {
transform-origin: 50% 50%; /* the default — center of the element */
transform: rotate(45deg);
}Syntax
transform-origin accepts one or two values (an x-offset and a y-offset), each as a percentage, a length, or a keyword (top, bottom, left, right, center). A third value can set a z-offset for 3D transforms.
.top-left { transform-origin: 0 0; } /* top-left corner */
.top-left-2 { transform-origin: top left; } /* same, as keywords */
.bottom-right { transform-origin: 100% 100%; } /* bottom-right corner */
.custom-point { transform-origin: 30px 10px; } /* an exact pixel point */
.right-edge { transform-origin: right center; } /* middle of the right edge */Same rotate(), Wildly Different Results
This is the clearest way to see why transform-origin matters: applying the exact same rotate(45deg) to three boxes, differing only in their origin point, produces three completely different visual results.
.rotate-center {
transform-origin: center; /* spins in place, corners sweep outward evenly */
transform: rotate(45deg);
}
.rotate-top-left {
transform-origin: top left; /* pivots around its own top-left corner —
the box swings down and to the right */
transform: rotate(45deg);
}
.rotate-bottom-right {
transform-origin: bottom right; /* pivots around its bottom-right corner —
the box swings up and to the left */
transform: rotate(45deg);
}With a center origin, the box rotates symmetrically in place. With a top left origin, that same corner stays perfectly still while the rest of the box swings around it like a clock hand — the box's overall position on the page visibly shifts even though nothing changed but the origin point.
Practical Use Case: A Flip Card Hinge
.flip-card {
perspective: 800px;
}
.flip-card__inner {
transition: transform 0.5s;
transform-style: preserve-3d;
transform-origin: left center; /* hinge along the left edge, like a page */
}
.flip-card:hover .flip-card__inner {
transform: rotateY(-140deg); /* swings open around the hinge, not the center */
}Setting the origin to left center makes the rotation read as a door or page swinging open along its left edge, instead of the more generic "spinning card" look a center origin would produce.
Practical Use Case: Zoom-From-Cursor on Hover
.zoomable {
transition: transform 0.3s ease;
transform-origin: var(--zoom-x, 50%) var(--zoom-y, 50%);
}
.zoomable:hover {
transform: scale(1.4);
}<div
class="zoomable"
onmousemove="
const r = this.getBoundingClientRect();
this.style.setProperty('--zoom-x', ((event.clientX - r.left) / r.width) * 100 + '%');
this.style.setProperty('--zoom-y', ((event.clientY - r.top) / r.height) * 100 + '%');
"
>
<img src="photo.jpg" alt="Product photo" />
</div>A tiny bit of JavaScript sets the origin's x/y percentage to track the cursor position via custom properties, so scale(1.4) zooms in centered on wherever the user's mouse is — the same technique used by most e-commerce "hover to zoom" product images.
Corners (
top left,bottom right, etc.) are the most common non-default choice, useful for menus or tooltips that should grow away from their trigger point.An edge midpoint (
left center,top center) is the classic hinge/flip setup.Dynamic, cursor-tracked origins (set via a custom property from JavaScript) power hover-to-zoom interactions.