transform 改变元素的绘制方式:移动、缩放、旋转或倾斜。下面的虚线框标出了每个方块在没有 transform 时所在的位置:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Transform functions</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.row { display: flex; gap: 40px; padding: 30px 20px; }
.slot { outline: 2px dashed #9ca3af; width: 90px; height: 60px; }
.box { width: 90px; height: 60px; background: #2563eb; color: white; display: grid; place-items: center; }
.t1 { transform: translate(20px, 10px); }
.t2 { transform: scale(1.3); background: #16a34a; }
.t3 { transform: rotate(20deg); background: #f59e0b; color: #111827; }
.t4 { transform: skewX(-20deg); background: #111827; }
</style>
</head>
<body>
<div class="row">
<div class="slot"><div class="box t1">translate</div></div>
<div class="slot"><div class="box t2">scale</div></div>
<div class="slot"><div class="box t3">rotate</div></div>
<div class="slot"><div class="box t4">skewX</div></div>
</div>
</body>
</html>
translate(20px, 10px) 把蓝色方块向右移动 20px、向下移动 10px。scale(1.3) 让绿色方块以中心为基准放大 30%。rotate(20deg) 把琥珀色方块顺时针旋转,skewX(-20deg) 让深色方块倾斜。无论哪种情况,虚线框(也就是元素在布局中占据的空间)都完全留在原处。
| 函数 | 作用 | 示例 |
|---|
translate(x, y) | 移动 | translate(20px, -10px), translate(-50%, -50%) |
translateX(x) / translateY(y) | 沿一个轴移动 | translateY(-4px) |
scale(s) / scale(x, y) | 缩放(1 = 原始大小,0.5 = 一半,负值为镜像) | scale(1.1), scale(-1, 1) |
scaleX(s) / scaleY(s) | 沿一个轴缩放 | scaleX(0.5) |
rotate(angle) | 顺时针旋转(负值为逆时针) | rotate(45deg), rotate(0.25turn) |
skew(x, y) / skewX(a) / skewY(a) | 倾斜 | skewX(-15deg) |
matrix(a, b, c, d, e, f) | 用一个矩阵表示以上所有变换,多半是浏览器返回给你的形式 | |
none | 不做变换 | |
translate() 中的百分比相对于元素自身的尺寸,而不是父元素的尺寸。旋转有单独的一页,讲角度、圈数和旋转动画:CSS rotate。
拖动滑块。第一行是你组合出的 transform 值;第二行是浏览器根据它计算出的矩阵,任何 transform 用 getComputedStyle 读取时返回的都是这个矩阵:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Transform playground</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.controls { display: grid; grid-template-columns: repeat(2, max-content); gap: 4px 16px; }
.stage { height: 170px; display: grid; place-items: center; background: #f3f4f6; margin: 10px 0; overflow: hidden; }
#box { width: 80px; height: 80px; background: #2563eb; color: white; display: grid; place-items: center; border-radius: 6px; }
code { display: block; }
</style>
</head>
<body>
<div class="controls">
<label>translateX <input type="range" data-f="translateX" data-u="px" min="-120" max="120" value="0"></label>
<label>scale <input type="range" data-f="scale" data-u="" min="0.5" max="1.8" step="0.1" value="1"></label>
<label>rotate <input type="range" data-f="rotate" data-u="deg" min="-180" max="180" value="0"></label>
<label>skewX <input type="range" data-f="skewX" data-u="deg" min="-40" max="40" value="0"></label>
</div>
<div class="stage"><div id="box">Box</div></div>
<code id="value"></code>
<code id="matrix"></code>
<script>
const box = document.getElementById('box');
const inputs = document.querySelectorAll('input');
const update = () => {
const t = [...inputs].map((i) => i.dataset.f + '(' + i.value + i.dataset.u + ')').join(' ');
box.style.transform = t;
document.getElementById('value').textContent = 'transform: ' + t + ';';
document.getElementById('matrix').textContent = 'computed: ' + getComputedStyle(box).transform;
};
inputs.forEach((i) => i.addEventListener('input', update));
update();
</script>
</body>
</html>
被变换的元素绘制在别的位置,但页面的布局就像它没有被变换一样。它的相邻元素保持原位。这正是 transform 适合做悬停效果和动画的原因:周围的东西都不会重新排版。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>No reflow</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.row { display: flex; gap: 10px; padding: 24px 10px; }
.row div { width: 80px; height: 50px; background: #e5e7eb; display: grid; place-items: center; }
.row .grow-t { transform: scale(1.5); background: #16a34a; color: white; }
.row .grow-w { width: 120px; height: 75px; background: #f59e0b; }
</style>
</head>
<body>
<div class="row" id="a"><div>A</div><div class="grow-t">scale(1.5)</div><div id="c1">C</div></div>
<div class="row" id="b"><div>A</div><div class="grow-w">width 120px</div><div id="c2">C</div></div>
<p id="out"></p>
<script>
document.getElementById('out').textContent =
'C starts at x = ' + document.getElementById('c1').offsetLeft + 'px next to the scaled box, and at x = ' +
document.getElementById('c2').offsetLeft + 'px next to the resized one.';
</script>
</body>
</html>
中间的两个方块看起来都大了 1.5 倍。被缩放的那个盖住了相邻元素,C 保持不动;用 width 和 height 放大的那个则把 C 往后推了 40px。
把多个函数写在同一个 transform 里,用空格分隔。顺序会改变结果,因为从左往右读,每个函数都在前面函数留下的坐标系中工作:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Transform order</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.stage { position: relative; height: 170px; background: #f3f4f6; }
.box { position: absolute; top: 20px; left: 20px; width: 60px; height: 40px; opacity: 0.85; }
.a { transform: translateX(120px) rotate(45deg); background: #2563eb; }
.b { transform: rotate(45deg) translateX(120px); background: #16a34a; }
</style>
</head>
<body>
<div class="stage"><div class="box a"></div><div class="box b"></div></div>
<p id="out"></p>
<script>
const center = (el) => { const r = el.getBoundingClientRect(); return Math.round(r.left + r.width / 2) + ', ' + Math.round(r.top + r.height / 2); };
document.getElementById('out').textContent =
'blue (translate then rotate) center: ' + center(document.querySelector('.a')) +
' | green (rotate then translate) center: ' + center(document.querySelector('.b'));
</script>
</body>
</html>
蓝色方块先直线移动了 120px,然后在原地旋转。绿色方块先旋转,所以它的“x 轴”指向斜下方,translateX(120px) 让它沿对角线向下移动。同样的函数,不同的位置。
另一个陷阱:第二条 transform 声明会替换第一条。如果卡片有 transform: rotate(-2deg),而它的悬停规则写的是 transform: scale(1.05),悬停时旋转就消失了。要么把两个都写上(rotate(-2deg) scale(1.05)),要么使用下面介绍的独立属性。
缩放、旋转和倾斜都围绕一个固定点进行,这个点就是变换原点,默认是中心。可以用关键字、长度或百分比修改它:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>transform-origin</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.row { display: flex; gap: 50px; padding: 20px 30px 40px; }
.slot { outline: 2px dashed #9ca3af; width: 80px; height: 50px; }
.box { width: 80px; height: 50px; background: #2563eb; transform: rotate(25deg); }
.tl { transform-origin: top left; }
.br { transform-origin: bottom right; }
</style>
</head>
<body>
<div class="row">
<div><code>center</code><div class="slot"><div class="box"></div></div></div>
<div><code>top left</code><div class="slot"><div class="box tl"></div></div></div>
<div><code>bottom right</code><div class="slot"><div class="box br"></div></div></div>
</div>
</body>
</html>
三个方块都旋转了 25 度。第一个绕中心旋转,第二个的左上角固定在虚线框的角上,第三个固定的是右下角。从按钮里展开的下拉菜单,就是用 transform-origin: top 配合 scale() 实现的。
translate、scale 和 rotate 属性
现代浏览器还提供了三个独立的属性,作用与对应的函数相同,但可以分别设置、分别做动画:
.card {
rotate: -2deg;
transition: scale 0.2s ease-out;
}
.card:hover {
scale: 1.05; /* the rotation stays, because it is a different property */
}
无论你按什么顺序声明,它们总是按 transform: translate(...) rotate(...) scale(...) 的顺序组合,并排在任何 transform 值之前。注意语法:translate: 10px 20px 和 scale: 1.2 的值不带函数括号。
用 translate(-50%, -50%) 居中
因为 translate() 的百分比相对于元素自身的尺寸,top: 50%; left: 50%; transform: translate(-50%, -50%) 可以让任意尺寸的绝对定位元素在父元素中居中。div 居中页面把它和 flexbox、grid 做了对比。
常见错误
- 写了两条
transform 声明。后一条会完全覆盖前一条。把函数合并到一起,或者使用 rotate、scale 和 translate 属性。
- 对行内元素做变换。普通的
<span> 或 <a> 会忽略 transform。把它设为 inline-block。
- 以为相邻元素会跟着移动。transform 从不改变布局。要腾出空间,应修改
width、margin 或 gap。
- 函数顺序写错。
rotate() 写在 translate() 前面,会沿旋转后的轴移动。
position: fixed 子元素出现意外。被变换的元素会成为固定定位后代元素的包含块,所以放在里面的固定头部会跟着它滚动。它还会为 z-index 创建新的层叠上下文。