CSS Module 16 – Transitions, animations, scroll and view transitions
Transforms: translate, rotate, scale and 3D
Move, rotate and scale elements with transform and the individual properties, why function order matters, what transforms change, and simple 3D.
What you will learn
- Move, rotate and scale elements with the translate, rotate and scale properties and with transform
- Predict how transform-origin and the order of transform functions change where an element is drawn
- Explain what transforms leave alone, the layout, and what they change, stacking and fixed positioning
- Build a 3D card flip with perspective, transform-style and backface-visibility that works without motion
Before you start
What you’ll learn · Before you start
What you will learn
- Move, rotate and scale elements with the translate, rotate and scale properties and with transform
- Predict how transform-origin and the order of transform functions change where an element is drawn
- Explain what transforms leave alone, the layout, and what they change, stacking and fixed positioning
- Build a 3D card flip with perspective, transform-style and backface-visibility that works without motion
Before you start
On this page
A transform changes where and how an element is drawn, moving it, turning it, resizing it or tilting it in 3D,
without changing the layout around it. Four properties do the work. translate moves an element, as in
translate: 0 -4px for a lift on hover. rotate turns it, as in rotate: 45deg. scale resizes it, as in
scale: 1.05. transform takes a list of functions that do the same jobs and more, such as
transform: rotate(45deg) translateX(100px). The neighbours of a transformed element stay exactly where they were,
which is why transforms are the cheap way to animate, and also why a scaled element can overlap them. For 3D,
perspective sets how far away the viewer is and backface-visibility hides the side of an element that faces away,
which is all a card flip needs. The positions and sizes in the tables come straight from getBoundingClientRect()
in three browser engines (Chromium 153, Firefox 155, WebKit 26.6), beginning with the result that surprises most
people: the same functions in another order put the element somewhere else.
translate, rotate, scale and the transform list
The three individual properties are Baseline widely available, and they are usually the clearer choice. translate
takes up to three lengths, and percentages of the element’s own size, so translate: -50% -50% moves an element up
and left by half of itself. rotate takes an angle, and optionally an axis for 3D, as in rotate: y 180deg. scale
takes one factor for both directions or one each, as in scale: 2 1. Because each is a property of its own, states
combine without repeating each other:
/* With the individual properties, each part of the motion has its own property, so states combine freely. */
.card {
transition: translate 200ms ease-out, scale 200ms ease-out;
}
.card:hover,
.card:focus-within {
translate: 0 -4px; /* lift */
scale: 1.02; /* and grow a little */
}
.card.is-new {
rotate: -2deg; /* a tilt that the hover rule above leaves alone */
}
/* The same states with one transform list: every combination has to repeat every function, in the right order. */
.card-old:hover,
.card-old:focus-within {
transform: translateY(-4px) scale(1.02);
}
.card-old.is-new {
transform: rotate(-2deg);
}
.card-old.is-new:hover,
.card-old.is-new:focus-within {
transform: translateY(-4px) rotate(-2deg) scale(1.02);
} The transform list can do more: skew(), matrix(), any number of functions, and a different order for each
element. The cost shows in the second half of that file. One transform value replaces another completely, so every
combination of states needs its own rule that repeats all the functions, and forgetting one drops the tilt or the
lift. Use the individual properties for the everyday moves and transform for the rare effect that needs a specific
sequence. Both can be used on the same element, and then the order is fixed, as the next section measures.
The same functions in another order
transform applies its functions from left to right, and each one works in the coordinate system that the functions
before it have already moved, rotated or scaled. So rotate(45deg) translateX(100px) first turns the box and its x
axis by 45°, then moves the box 100px along that turned axis, down and to the right.
translateX(100px) rotate(45deg) first moves the box 100px to the right, then turns it where it stands.
The same two transform functions, in two orders
Text description of the diagram
The diagram shows two sequences for a 100 by 50 pixel box whose centre starts at 250, 225.
First, transform: rotate(45deg) translateX(100px). Step 1, rotate(45deg): the box and its x axis turn 45 degrees around the box's centre. Step 2, translateX(100px): the box moves 100 pixels along the turned x axis, which points down and to the right. Its centre is drawn at 320.7, 295.7.
Second, transform: translateX(100px) rotate(45deg). Step 1, translateX(100px): the box moves 100 pixels to the right. Step 2, rotate(45deg): it turns 45 degrees where it now stands. Its centre is drawn at 350, 225.
A dashed arrow between the two sequences notes that they use the same functions in a different order.
This program does the arithmetic with matrices for a 100 × 50 box at x 200, y 200, turning around its centre:
// Where a 100 × 50 box at x 200, y 200 ends up for two orders of the same two functions, worked out with 2D matrices
// the way CSS Transforms does it: functions apply from left to right, around the transform-origin (the centre here).
const multiply = ([a, b, c, d, e, f], [g, h, i, j, k, l]) =>
[a * g + c * h, b * g + d * h, a * i + c * j, b * i + d * j, a * k + c * l + e, b * k + d * l + f];
const translate = (x, y = 0) => [1, 0, 0, 1, x, y];
const rotate = (deg) => {
const r = (deg * Math.PI) / 180;
return [Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r), 0, 0];
};
const apply = ([a, b, c, d, e, f], [x, y]) => [a * x + c * y + e, b * x + d * y + f];
const box = { x: 200, y: 200, width: 100, height: 50 };
const origin = [box.x + box.width / 2, box.y + box.height / 2];
function place(name, functions) {
// translate to the origin, apply the list left to right, translate back
let m = translate(...origin);
for (const fn of functions) m = multiply(m, fn);
m = multiply(m, translate(-origin[0], -origin[1]));
const corners = [[box.x, box.y], [box.x + box.width, box.y], [box.x, box.y + box.height], [box.x + box.width, box.y + box.height]]
.map((p) => apply(m, p));
const xs = corners.map(([x]) => x), ys = corners.map(([, y]) => y);
const centre = apply(m, origin).map((v) => v.toFixed(1));
const size = [Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)].map((v) => v.toFixed(1));
console.log(`${name.padEnd(28)} centre at ${centre.join(', ')}; bounding box ${size.join(' × ')}`);
}
place('rotate(45deg) translateX(100px)', [rotate(45), translate(100)]);
place('translateX(100px) rotate(45deg)', [translate(100), rotate(45)]); Output
rotate(45deg) translateX(100px) centre at 320.7, 295.7; bounding box 106.1 × 106.1 translateX(100px) rotate(45deg) centre at 350.0, 225.0; bounding box 106.1 × 106.1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node order.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The page below draws four boxes and asks the browser where it put each one:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The order of transform functions</title>
<style>
body { margin: 0; }
.box { position: absolute; inset-block-start: 200px; inset-inline-start: 200px; inline-size: 100px; block-size: 50px;
background: #0e7490; }
#rotate-first { transform: rotate(45deg) translateX(100px); }
#translate-first { transform: translateX(100px) rotate(45deg); }
#individual { translate: 100px; rotate: 45deg; } /* the individual properties: translate, then rotate */
#mixed { rotate: 45deg; transform: translateX(100px); } /* rotate first, because transform always comes last */
</style>
</head>
<body>
<div class="box" id="rotate-first"></div>
<div class="box" id="translate-first"></div>
<div class="box" id="individual"></div>
<div class="box" id="mixed"></div>
<script>
// Every box starts at x 200, y 200 with its origin at its centre (250, 225). Where does each one end up?
for (const box of document.querySelectorAll('.box')) {
const r = box.getBoundingClientRect();
const centre = [(r.left + r.right) / 2, (r.top + r.bottom) / 2].map((v) => v.toFixed(1));
console.log(`${box.id}: centre at ${centre.join(', ')}; bounding box ${r.width.toFixed(1)} × ${r.height.toFixed(1)}`);
}
</script>
</body>
</html> | The box has | Centre drawn at | Bounding box |
|---|---|---|
transform: rotate(45deg) translateX(100px) |
320.7, 295.7 | 106.1 × 106.1 |
transform: translateX(100px) rotate(45deg) |
350.0, 225.0 | 106.1 × 106.1 |
translate: 100px; rotate: 45deg |
350.0, 225.0 | 106.1 × 106.1 |
rotate: 45deg; transform: translateX(100px) |
320.7, 295.7 | 106.1 × 106.1 |
All three engines matched the program. The last two rows show the fixed order of the individual properties: CSS
always applies translate, then rotate, then scale, and only then the functions of transform, whatever order the
declarations are written in. So rotate with a transform that translates behaves like the rotate-first list. The
bounding box, what getBoundingClientRect() reports, is the rectangle around the turned box: 106.1 px square for a
100 × 50 box at 45°.
transform-origin
Rotation and scaling happen around the transform origin, the point that stays still. It defaults to the centre of
the element’s border box, 50% 50%. A 100 × 50 box turned by 90° around its centre ends up 50 wide and 100 tall with
the same centre; turned around transform-origin: 0 0, its top-left corner, it swings out to the left of where it was,
measured at x 150 to 200 and y 200 to 300 in all three engines. A clock hand turns around one end and a bell swings
from its top, so set the origin to the point the real object would hang from. For SVG shapes, transform-box picks
the box the origin refers to; it is Baseline newly available.
What a transform changes, and what it leaves alone
A transform happens after layout. The browser sizes and places every box first, then draws the transformed element
moved, turned or resized, so nothing else on the page moves to make room. zoom is the exception that proves the
rule: it scales an element before layout, so the layout grows with it. Two tiles in a row, measured:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>What a transform changes, and what it leaves alone</title>
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.row { display: flex; gap: 8px; margin: 8px; }
.tile { inline-size: 50px; block-size: 50px; background: #4338ca; }
#scaled { scale: 2; }
#zoomed { zoom: 2; }
.panel { margin: 8px; block-size: 100px; background: #e0e7ff; }
.panel .pin { position: fixed; inset-block-start: 0; inset-inline-start: 0; inline-size: 40px; block-size: 20px;
background: #be123c; }
#moved { translate: 0 0; } /* a translate of zero still changes the rules for its descendants */
.spacer { block-size: 2000px; }
</style>
</head>
<body>
<div class="row"><div class="tile" id="plain"></div><div class="tile next"></div></div>
<div class="row"><div class="tile" id="scaled"></div><div class="tile next"></div></div>
<div class="row"><div class="tile" id="zoomed"></div><div class="tile next"></div></div>
<div class="panel" id="still"><div class="pin"></div></div>
<div class="panel" id="moved"><div class="pin"></div></div>
<div class="spacer"></div>
<script>
// The tile after each first tile: where does it start? scale draws the first tile bigger; zoom lays it out bigger.
for (const row of document.querySelectorAll('.row')) {
const first = row.firstElementChild;
const next = row.querySelector('.next');
const box = first.getBoundingClientRect();
console.log(`${first.id}: drawn ${box.width} × ${box.height}px; the next tile starts at x ${next.getBoundingClientRect().left}px`);
}
// A position: fixed child normally stays put when the page scrolls. Inside a transformed parent it scrolls away.
scrollTo(0, 150);
for (const panel of document.querySelectorAll('.panel')) {
const pin = panel.querySelector('.pin');
console.log(`fixed child in the ${panel.id} panel, page scrolled 150px: top ${pin.getBoundingClientRect().top}px`);
}
</script>
</body>
</html> | The first tile has | Drawn size | The next tile starts at |
|---|---|---|
| nothing | 50 × 50px | x 66px |
scale: 2 |
100 × 100px | x 66px, now overlapped |
zoom: 2 |
100 × 100px | x 116px, pushed along |
zoom is Baseline newly available. Use it when the layout should grow, such as a magnified preview inside a
document, and transforms when it shouldn’t, such as a hover effect.
Transforms do change two things, and both catch people out. Any value other than none, even translate: 0, makes
the element a stacking context, so z-index inside it no longer competes with the rest of the page. It also makes
the element the containing block for fixed-position descendants, so a position: fixed child stops being fixed to
the screen. In the page above, after scrolling 150px, a fixed child of a plain panel stayed at the top of the window
(top 0px), while the same child in a panel with translate: 0 0 had scrolled away with its parent (top 190px), in all
three engines. A modal or a sticky toolbar written as position: fixed breaks the moment an animation library puts a
transform on one of its ancestors.
3D: perspective, preserve-3d and backface-visibility
3D transforms are Baseline widely available. Three properties turn a flat page into a scene:
perspectiveon a parent sets the distance between the viewer and the page. A smaller value means stronger depth: an element 100px closer than the page is drawn 600 ÷ (600 − 100) = 1.2 times larger withperspective: 600px.perspective-originmoves the point the viewer looks from.transform-style: preserve-3dlets an element’s children keep their own positions in depth, so they can be arranged in front of and behind each other inside it. The default,flat, draws the children flat into their parent, like a picture on a card.backface-visibility: hiddenstops an element from being drawn when its back faces the viewer, which is how a card shows a different face on each side.
preserve-3d is fragile. Some properties need the element and its children to be drawn together as one flat group,
and the specification lists them: overflow other than visible or clip, opacity below 1, any filter,
clip-path, mask-image, mix-blend-mode, isolation: isolate and paint containment. One of them on the same
element silently flattens it, although getComputedStyle() still says preserve-3d.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>When preserve-3d is flattened</title>
<style>
body { margin: 0; }
.scene { perspective: 600px; inline-size: 300px; block-size: 200px; margin: 20px; }
.stage { transform-style: preserve-3d; inline-size: 300px; block-size: 200px; }
.layer { inline-size: 100px; block-size: 100px; margin: 50px 100px; background: #9333ea; translate: 0 0 100px; }
#clipped .stage { overflow: hidden; }
#faded .stage { opacity: 0.99; }
#clip-ok .stage { overflow: clip; }
</style>
</head>
<body>
<div class="scene" id="preserved"><div class="stage"><div class="layer"></div></div></div>
<div class="scene" id="clipped"><div class="stage"><div class="layer"></div></div></div>
<div class="scene" id="faded"><div class="stage"><div class="layer"></div></div></div>
<div class="scene" id="clip-ok"><div class="stage"><div class="layer"></div></div></div>
<script>
// The layer is 100px wide and 100px closer to the viewer. Inside a 3D scene with perspective: 600px it should be
// drawn 600 ÷ (600 − 100) = 1.2 times as wide. A flattened stage draws it flat instead.
for (const scene of document.querySelectorAll('.scene')) {
const layer = scene.querySelector('.layer');
const used = getComputedStyle(scene.querySelector('.stage')).transformStyle;
console.log(`${scene.id}: the layer is drawn ${layer.getBoundingClientRect().width.toFixed(1)}px wide (transform-style computes to ${used})`);
}
</script>
</body>
</html> | The stage has | Layer drawn |
|---|---|
preserve-3d only |
120.0px wide, in depth |
overflow: hidden too |
100.0px wide, flat |
opacity: 0.99 too |
100.0px wide, flat |
overflow: clip too |
100.0px wide, flat |
The first three rows are what the specification predicts. The last is not: CSS Transforms Level 2 exempts
overflow: clip, yet all three engines flattened the stage anyway, so don’t count on that exception. When a 3D effect
suddenly looks flat, look for one of these properties on the element with preserve-3d, often added for an unrelated
reason, such as overflow: hidden to round the corners.
A card flip that works for everyone
The flip puts two faces in the same grid cell, turns the back one away with rotate: y 180deg, hides whichever face
points away, and turns the whole card on hover, on keyboard focus inside it and from a real button for touch screens:
/* A card that turns over on hover, on keyboard focus and from its button, and swaps at once for reduced motion. */
.flip-card {
perspective: 800px; /* the viewer's distance from the card: closer means stronger depth */
}
.flip-card-inner {
display: grid;
transform-style: preserve-3d; /* the faces keep their own 3D positions inside the turning card */
transition: rotate 500ms ease-in-out;
}
.flip-card-face {
grid-area: 1 / 1; /* both faces in the same cell, one on top of the other */
backface-visibility: hidden; /* a face turned away from the viewer is not drawn */
}
.flip-card-back {
rotate: y 180deg; /* starts turned away, so it faces the viewer once the card has turned */
}
.flip-card:hover .flip-card-inner,
.flip-card:focus-within .flip-card-inner,
.flip-card.is-flipped .flip-card-inner {
rotate: y 180deg;
}
@media (prefers-reduced-motion: reduce) {
.flip-card-inner {
transition: none; /* the faces swap at once; nothing turns */
}
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A flip card, measured</title>
<link rel="stylesheet" href="flip-card.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.flip-card { inline-size: 240px; margin: 24px; }
.flip-card-face { padding: 16px; min-block-size: 120px; background: #ecfeff; }
.flip-card-back { background: #164e63; color: #fff; }
</style>
</head>
<body>
<div class="flip-card" id="card">
<div class="flip-card-inner">
<div class="flip-card-face flip-card-front">
<h2>Plan A</h2>
<button type="button" aria-pressed="false">Show what is included</button>
</div>
<div class="flip-card-face flip-card-back">
<p>Included: 3 projects, weekly backups and email support.</p>
</div>
</div>
</div>
<script>
const card = document.getElementById('card');
const inner = card.querySelector('.flip-card-inner');
const button = card.querySelector('button');
button.addEventListener('click', () => {
const flipped = card.classList.toggle('is-flipped');
button.setAttribute('aria-pressed', String(flipped));
});
// Which face is under the middle of the card? backface-visibility: hidden takes a turned-away face out of the
// hit test too, so this shows which face people can see and click.
const faceAtCentre = () => {
const r = card.getBoundingClientRect();
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height - 10);
return hit?.closest('.flip-card-face')?.classList.contains('flip-card-back') ? 'back' : 'front';
};
const settle = () => { for (const a of inner.getAnimations()) a.finish(); };
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduced motion' : 'full motion';
console.log(`${reduce}, at rest: rotate ${getComputedStyle(inner).rotate}, ${faceAtCentre()} face shows`);
button.click();
const running = inner.getAnimations().length;
settle();
console.log(`${reduce}, after the button: rotate ${getComputedStyle(inner).rotate}, ${faceAtCentre()} face shows, ` +
`${running ? 'it turned' : 'it swapped at once'}, aria-pressed ${button.getAttribute('aria-pressed')}`);
const faces = [...card.querySelectorAll('.flip-card-face')].map((f) => getComputedStyle(f).backfaceVisibility);
console.log(`backface-visibility of the two faces: ${faces.join(', ')}`);
</script>
</body>
</html> The page asks the browser which face is under the middle of the card, the face a person could see and click. In all
three engines the front was there at rest and the back after the button was pressed, with rotate computed as
y 180deg and aria-pressed updated to true; a turned-away face with backface-visibility: hidden is left out of
hit testing as well as drawing. Under Playwright’s reduced-motion emulation the same press started no animation: the
faces swapped at once. Keep the back’s content meaningful on its own, because screen readers read both faces in
source order whichever one is showing, and never put the only copy of important information behind a hover.
Practise: transform a box
Exercise · Medium · JavaScript
Work out where a transformed box is drawn
Browsers draw a transformed box by moving each of its corners, then take the smallest rectangle around them for getBoundingClientRect(). Write three functions in transform.mjs and export them. A transform function is an array: ['translate', x, y] (y defaults to 0), ['rotate', degrees] (clockwise on screen, because y grows downwards) or ['scale', sx, sy] (sy defaults to sx).
transformPoint([x, y], functions, origin = [0, 0]) returns where the point is drawn. Work relative to the origin: subtract it, apply the functions from the last one to the first, then add the origin back. Applying from the right is what makes rotate(45deg) translateX(100px) move the box along a 45° line: the translation happens inside the rotated coordinate system. Rotating [x, y] by an angle a gives [x × cos a − y × sin a, x × sin a + y × cos a].
individualOrder({ translate, rotate, scale, transform = [] }) returns the single list of functions that the individual properties stand for. Each property is optional: translate is [x, y], rotate a number of degrees and scale [sx, sy]. CSS always applies them in the same order, translate, then rotate, then scale, then the functions of transform, whatever order you write them in.
boundingBox({ x, y, width, height }, functions, origin) returns { left, top, right, bottom } of the rectangle around the four transformed corners. The origin defaults to the box's centre, like transform-origin: 50% 50%.
The sample tests import the three functions from transform.mjs and run in your browser. Their expected boxes were measured with getBoundingClientRect() in three browser engines.
Starter code · transform.mjs
/** Where the point [x, y] is drawn after the transform functions, around the origin. */
export function transformPoint([x, y], functions, origin = [0, 0]) {
// Replace this line with your code.
return [x, y];
}
/** The transform functions that translate, rotate, scale and transform stand for, in the order CSS applies them. */
export function individualOrder({ translate, rotate, scale, transform = [] }) {
// Replace this line with your code.
return transform;
}
/** The rectangle around the box after the transform functions; the origin defaults to the box's centre. */
export function boundingBox({ x, y, width, height }, functions, origin) {
// Replace this line with your code.
return { left: x, top: y, right: x + width, bottom: y + height };
} The sample tests · transform.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { transformPoint, individualOrder, boundingBox } from './transform.mjs';
const r = (v) => Math.round(v * 10) / 10 + 0; // one decimal, and no -0
const rp = (p) => p.map(r);
const rb = (b) => ({ left: r(b.left), top: r(b.top), right: r(b.right), bottom: r(b.bottom) });
const box = { x: 200, y: 200, width: 100, height: 50 };
const centre = [250, 225];
test('the same two functions in two orders', () => {
assert.deepEqual(rp(transformPoint(centre, [['rotate', 45], ['translate', 100]], centre)), [320.7, 295.7]);
assert.deepEqual(rp(transformPoint(centre, [['translate', 100], ['rotate', 45]], centre)), [350, 225]);
});
test('rotation and scale around the origin', () => {
assert.deepEqual(rp(transformPoint([200, 200], [['rotate', 90]], centre)), [275, 175]);
assert.deepEqual(rp(transformPoint([200, 200], [['rotate', 90]], [200, 200])), [200, 200]);
assert.deepEqual(rp(transformPoint([300, 250], [['scale', 2]], centre)), [350, 275]);
assert.deepEqual(rp(transformPoint([300, 250], [['scale', 2, 1]], centre)), [350, 250]);
assert.deepEqual(rp(transformPoint([10, 20], [['translate', 5, -5]])), [15, 15]);
});
test('the individual properties always apply translate, rotate, scale, then transform', () => {
assert.deepEqual(individualOrder({ rotate: 45, translate: [100, 0] }), [['translate', 100, 0], ['rotate', 45]]);
assert.deepEqual(individualOrder({ scale: [2, 2], rotate: 30, translate: [20, 10] }),
[['translate', 20, 10], ['rotate', 30], ['scale', 2, 2]]);
assert.deepEqual(individualOrder({ rotate: 45, transform: [['translate', 100, 0]] }), [['rotate', 45], ['translate', 100, 0]]);
assert.deepEqual(individualOrder({}), []);
});
test('bounding boxes measured in three engines', () => {
assert.deepEqual(rb(boundingBox(box, [['rotate', 45], ['translate', 100]])), { left: 267.7, top: 242.7, right: 373.7, bottom: 348.7 });
assert.deepEqual(rb(boundingBox(box, [['translate', 100], ['rotate', 45]])), { left: 297, top: 172, right: 403, bottom: 278 });
assert.deepEqual(rb(boundingBox(box, [['rotate', 90]], [200, 200])), { left: 150, top: 200, right: 200, bottom: 300 });
});
test('individual properties against the same functions in transform', () => {
const individual = individualOrder({ scale: [2, 2], rotate: 30, translate: [20, 10] });
assert.deepEqual(rb(boundingBox(box, individual)), { left: 158.4, top: 141.7, right: 381.6, bottom: 328.3 });
assert.deepEqual(rb(boundingBox(box, [['scale', 2], ['rotate', 30], ['translate', 20, 10]])), { left: 163, top: 169, right: 386.2, bottom: 355.6 });
}); A hint
In transformPoint, loop over the functions with an index that counts down, from functions.length - 1 to 0, and update [x, y] at each step. boundingBox can then call transformPoint on the four corners and take the smallest and largest x and y.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between transform: translateX(100px) and left: 100px on a positioned element?
Both move the element 100px to the right on screen, but at different stages. left is part of layout: it changes
where the element’s box is placed, so the browser must lay out again whenever it changes. translate and the other
transforms happen after layout: the box keeps its place, the element is drawn moved, and nothing around it reflows,
which is why moving it with a transform is cheaper to animate. Percentages differ as well: in left they refer to the
containing block’s width, in translateX() to the element’s own width. A transform also makes the element a stacking
context and the containing block of its fixed-position descendants, which left does not. getBoundingClientRect()
reports the moved position for both, while offsetLeft only changes with left.
Key takeaways
translate,rotateandscaleare separate properties that combine freely; CSS applies them in that fixed order, then thetransformlist.- In
transform, order matters:rotate(45deg) translateX(100px)andtranslateX(100px) rotate(45deg)put the same box with centres 29.3 px apart horizontally and 70.7 px apart vertically. - Rotation and scale happen around
transform-origin, the centre by default. - Transforms never move neighbours;
zoomdoes. Any transform creates a stacking context and capturesposition: fixeddescendants. perspective,preserve-3dandbackface-visibilityare enough for a card flip;overflow,opacitybelow 1 and filters flattenpreserve-3d, and so didoverflow: clipin all three engines.- Flip on hover, on focus and from a button, and swap the faces at once for reduced motion.
References
- CSS Transforms Module Level 1 (W3C CSS Working Group)
- CSS Transforms Module Level 2: individual transform properties (W3C CSS Working Group)
- CSS Transforms Module Level 2: grouping property values (W3C CSS Working Group)
- CSS Viewport Module Level 1: the zoom property (W3C CSS Working Group)
- Media Queries Level 5: prefers-reduced-motion (W3C CSS Working Group)
- backface-visibility (MDN Web Docs) (Mozilla)
- transform-style (MDN Web Docs) (Mozilla)
- zoom (MDN Web Docs) (Mozilla)
- Web features explorer: individual transform properties (W3C WebDX Community Group)
- Web features explorer: 3D transforms (W3C WebDX Community Group)
- Web features explorer: zoom (W3C WebDX Community Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress