Transforms
Transforms change how an element is drawn: you can move it, turn it, make it bigger or smaller, or slant it. They are behind countless everyday effects, such as a card that lifts on hover, an arrow that turns when a menu opens or a photo that zooms slightly when you point at it.
In this lesson you will learn the transform property and its functions, the newer individual translate, rotate and scale properties, transform-origin, and a first taste of 3D.
What transforms do, and what they do not
A transform changes only the drawing of an element, not its place in the layout. If you move a box 50px to the right with a transform, the space it originally took stays reserved, and nothing around it moves. The element simply paints somewhere else.
That is why transforms are perfect for animation. The browser does not need to recalculate the layout of the page on every frame, so transformed animations stay smooth even on modest phones.
The transform property
The transform property takes one or more transform functions:
| Function | What it does | Example |
|---|---|---|
translate(x, y) | Moves the element | translate(20px, -10px) |
rotate(angle) | Turns it around a point | rotate(15deg), rotate(0.5turn) |
scale(x, y) | Makes it bigger or smaller | scale(1.2), scale(1, 0.5) |
skew(x, y) | Slants it | skew(10deg) |
Angles can be written in deg (degrees, 360 in a full circle) or turn (1turn is a full circle). A scale of 1 is the normal size, 2 is double and 0.5 is half.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Transform functions</title>
<style>
body { font-family: system-ui, sans-serif; margin: 24px; }
.row { display: flex; gap: 40px; flex-wrap: wrap; }
h2 { margin-top: 48px; }
.box {
width: 90px;
height: 90px;
display: grid;
place-items: center;
border-radius: 8px;
background: #6366f1;
color: white;
font-size: 0.85rem;
}
.moved { transform: translate(10px, 20px); }
.turned { transform: rotate(20deg); }
.bigger { transform: scale(1.25); }
.slanted { transform: skew(-12deg); }
.order-a { transform: translate(60px) rotate(45deg); background: #0d9488; }
.order-b { transform: rotate(45deg) translate(60px); background: #db2777; }
</style>
</head>
<body>
<div class="row">
<div class="box moved">translate</div>
<div class="box turned">rotate</div>
<div class="box bigger">scale</div>
<div class="box slanted">skew</div>
</div>
<h2>Order matters</h2>
<div class="row">
<div class="box order-a">A</div>
<div class="box order-b">B</div>
</div>
</body>
</html>Look at boxes A and B. They use the same two functions in a different order. The browser applies the functions from left to right, and each one works in the space left by the previous one. Box A moves 60px to the right and then turns. Box B turns first, so its "right" now points diagonally downwards, and the move follows that diagonal.
The individual transform properties
Writing everything in one transform causes a real problem. Suppose a card is rotated slightly, and on hover you want it to grow:
.photo { transform: rotate(-3deg); }
.photo:hover { transform: scale(1.1); } /* the rotation is lost! */The hover rule replaces the whole transform value, so the rotation disappears. You would have to repeat rotate(-3deg) scale(1.1) in the hover rule, and keep both in step forever.
CSS now has individual transform properties: translate, rotate and scale. Each one can be set, changed and animated on its own. They have worked in all major browsers since 2022.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Individual transforms</title>
<style>
body { font-family: system-ui, sans-serif; margin: 32px; }
.photos { display: flex; gap: 24px; flex-wrap: wrap; }
.photo {
width: 160px;
padding: 8px 8px 24px;
background: white;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.2);
rotate: -3deg;
transition: scale 200ms ease-out, rotate 200ms ease-out;
}
.photo:nth-child(2) { rotate: 4deg; }
.photo:hover {
scale: 1.1; /* the rotation stays */
}
.photo:focus-visible { rotate: 0deg; }
.photo img { width: 100%; display: block; }
.photo p { margin: 8px 0 0; text-align: center; }
</style>
</head>
<body>
<div class="photos">
<figure class="photo" tabindex="0">
<img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" alt="Hills and a river at sunrise">
<p>Sinhagad at sunrise</p>
</figure>
<figure class="photo" tabindex="0">
<img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" alt="Hills and a river at sunrise">
<p>River walk, Kochi</p>
</figure>
</div>
</body>
</html>Hover changes only scale, so each photo keeps its own tilt. The values are written without function brackets: translate: 10px 20px, rotate: 15deg, scale: 1.2 (or scale: 1.2 0.8 for different horizontal and vertical scaling). There is no individual skew property; use transform: skew() for that.
When you use more than one, the browser always applies them in a fixed order: translate first, then rotate, then scale, and finally anything in transform. So translate: 60px; rotate: 45deg; gives the same result as box A above, whatever order you write the lines in.
transform-origin
Rotation and scaling happen around a point called the transform origin. By default it is the centre of the element. Change it with transform-origin to make an element swing like a door or grow from one corner:
.door {
transform-origin: left center; /* hinge on the left edge */
transition: rotate 400ms ease-out;
}
.door.open {
rotate: y -70deg; /* swing open around the hinge */
}
.dropdown {
transform-origin: top; /* grows downwards from its top edge */
scale: 1 0;
}transform-origin accepts keywords (top, left, center), lengths and percentages. It affects the individual properties and transform alike.
Percentages in translate
Percentages in translate are based on the element's own size, not its parent's. translate: -50% 0 moves an element left by half of its own width. You will often see this in older code for centring:
.badge {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%; /* pull back by half its own size */
}Today display: grid; place-items: center; on the parent is usually simpler, but this trick is still useful for elements that must be positioned, as covered in Positioning.
A first look at 3D
Transforms also work in three dimensions. rotate: y 180deg turns an element around the vertical axis, like a page in a book. For it to look three-dimensional, the parent needs perspective, which sets how far the viewer is from the screen.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flip card</title>
<style>
body { font-family: system-ui, sans-serif; margin: 24px; }
.flip { width: 240px; height: 150px; perspective: 800px; }
.flip-inner {
position: relative;
height: 100%;
transform-style: preserve-3d;
transition: rotate 600ms ease-in-out;
}
.flip.flipped .flip-inner { rotate: y 180deg; }
.face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 12px;
backface-visibility: hidden;
color: white;
}
.front { background: #2563eb; }
.back { background: #16a34a; rotate: y 180deg; }
</style>
</head>
<body>
<div class="flip">
<div class="flip-inner">
<div class="face front">Capital of Kerala?</div>
<div class="face back">Thiruvananthapuram</div>
</div>
</div>
<p><button type="button" id="flip">Flip the card</button></p>
<script>
document.querySelector('#flip').addEventListener('click', () => {
document.querySelector('.flip').classList.toggle('flipped');
});
</script>
</body>
</html>transform-style: preserve-3d keeps the two faces in the same 3D space, and backface-visibility: hidden hides each face when it is turned away from you. The back face starts already turned by 180 degrees, so it faces you once the card flips.
Common mistakes
- Two
transformdeclarations. The second replaces the first completely. Use the individual properties, or list all functions in one value. - Transforming an inline element. Transforms do not apply to plain inline elements such as
<span>or<a>. Give themdisplay: inline-block. - Expecting other elements to move. Transforms do not change layout. If neighbours should move, change layout properties instead.
- Fixed children stop being fixed. A transformed element becomes the reference box for
position: fixedchildren, so a "fixed" header inside it will scroll with it.
Key points
- Transforms move, rotate, scale or skew how an element is drawn without changing the layout.
transformtakes a list of functions applied from left to right, so the order changes the result.- The individual properties
translate,rotateandscalecan be set and animated separately. - Individual properties always apply in the order translate, rotate, scale, then
transform. transform-originsets the point that rotation and scaling happen around.- Percentages in
translaterefer to the element's own size; 3D needsperspectiveon a parent.
Exercise
Create a "Menu" button with a small arrow (use the character > inside a <span>). When the button has the class open, the arrow should turn by 90 degrees over 200ms. Use the individual rotate property, remember that a <span> needs a display value that allows transforms, and add a few lines of JavaScript that toggle the class on click.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Menu arrow</title>
<style>
.menu-btn .arrow {
display: inline-block;
transition: rotate 200ms ease-out;
}
.menu-btn.open .arrow {
rotate: 90deg;
}
</style>
</head>
<body>
<button class="menu-btn" type="button">Menu <span class="arrow" aria-hidden="true">></span></button>
<script>
const btn = document.querySelector('.menu-btn');
btn.addEventListener('click', () => btn.classList.toggle('open'));
</script>
</body>
</html>The span is inline-block, so it can be transformed. The transition is on the normal rule, so the arrow turns back smoothly when the class is removed.
Quick Quiz
1. What happens to the space around an element when you move it with translate?
Transforms only change how the element is drawn, not the layout.
2. A card has rotate: 5deg. On hover you set scale: 1.1. What does the hovered card look like?
Individual transform properties are independent, so changing scale does not reset rotate.
3. Why does transform: rotate(45deg) translate(60px) move the element diagonally?
Functions apply from left to right, and each works in the space left by the previous one.
4. Which element cannot be transformed until you change its display value?
Plain inline boxes such as <span> ignore transforms; inline-block fixes it.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
