旋转一个元素
有两种写法,效果相同:transform 中的 rotate() 函数,或者单独的 rotate 属性。正角度顺时针旋转:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rotate</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.row { display: flex; gap: 50px; padding: 30px; }
.box { width: 90px; height: 60px; display: grid; place-items: center; color: white; }
.a { transform: rotate(45deg); background: #2563eb; }
.b { rotate: 45deg; background: #16a34a; }
.c { rotate: -15deg; background: #f59e0b; color: #111827; }
</style>
</head>
<body>
<div class="row">
<div class="box a">transform</div>
<div class="box b">rotate: 45deg</div>
<div class="box c">-15deg</div>
</div>
</body>
</html>
蓝色和绿色的盒子旋转方式完全相同,都绕自身中心顺时针旋转了 45 度。琥珀色盒子的角度是负值,所以它逆时针倾斜。旋转从不改变布局:每个盒子仍然占据原来 90 乘 60 的位置,这就是旋转后的元素可能盖住相邻元素的原因。
角度单位
| 单位 | 一整圈 | 90 度 |
|---|
deg | 360deg | 90deg |
turn | 1turn | 0.25turn |
rad | 6.2832rad | 1.5708rad |
grad | 400grad | 100grad |
deg 是最常用的选择;turn 用在旋转动画中读起来很直观(rotate: 1turn)。角度可以超过 360,这在动画中很有用:关键帧中的 rotate(720deg) 会转两圈。一定要写单位:rotate(45) 是无效的,会被忽略。
拖动滑块,查看同一个角度在各个单位下的值:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angle slider</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; }
.stage { display: grid; place-items: center; height: 160px; background: #f3f4f6; margin: 10px 0; }
#arrow { width: 110px; height: 14px; background: #2563eb; border-radius: 7px; position: relative; }
#arrow::after { content: ""; position: absolute; inset-inline-end: -10px; top: -8px; border: 15px solid transparent; border-inline-start: 18px solid #2563eb; border-inline-end: 0; }
</style>
</head>
<body>
<label>angle <input id="a" type="range" min="-360" max="360" value="30"></label>
<div class="stage"><div id="arrow"></div></div>
<p id="out"></p>
<script>
const input = document.getElementById('a');
const update = () => {
const deg = Number(input.value);
document.getElementById('arrow').style.rotate = deg + 'deg';
document.getElementById('out').textContent =
deg + 'deg = ' + (deg / 360).toFixed(3) + 'turn = ' + (deg * Math.PI / 180).toFixed(4) + 'rad';
};
input.addEventListener('input', update);
update();
</script>
</body>
</html>
元素默认绕自身中心旋转。transform-origin 可以移动旋转的支点。时钟的指针是绕它的一端旋转的,而不是绕中间:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>transform-origin</title>
<style>
.face { position: relative; width: 160px; height: 160px; border-radius: 50%; background: #e5e7eb; }
.hand {
position: absolute;
left: 76px; /* 80px center minus half the 8px width */
top: 20px;
width: 8px;
height: 60px;
border-radius: 4px;
background: #111827;
transform-origin: bottom center; /* pivot at the center of the face */
rotate: 120deg;
}
.pin { position: absolute; left: 74px; top: 74px; width: 12px; height: 12px; border-radius: 50%; background: #f59e0b; }
</style>
</head>
<body>
<div class="face"><div class="hand" id="hand"></div><div class="pin"></div></div>
</body>
</html>
指针的底端落在表盘中心的琥珀色圆钉上,rotate: 120deg 把它转到四点钟的方向。删掉 transform-origin 这一行,指针就会改为绕自己的中点旋转,离开圆钉。
旋转文字与竖向标签
对徽标或贴纸用 rotate 做一点小倾斜没有问题。但真正的竖向标签更适合用 writing-mode,因为它会改变布局:元素变得又高又窄,而不是盖住旁边的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vertical text</title>
<style>
body { font-family: system-ui, sans-serif; padding-top: 24px; }
.row { display: flex; gap: 12px; align-items: flex-start; }
.label { background: #2563eb; color: white; padding: 6px 10px; }
.rotated { rotate: -90deg; }
.vertical { writing-mode: vertical-rl; rotate: 180deg; }
.content { background: #e5e7eb; padding: 10px; }
.badge { display: inline-block; rotate: -6deg; background: #f59e0b; padding: 4px 10px; margin: 16px 0 0 8px; }
</style>
</head>
<body>
<div class="row"><div class="label rotated" id="r">Section</div><div class="content">rotate: -90deg overlaps</div></div>
<div class="row" style="margin-top: 20px;"><div class="label vertical" id="v">Section</div><div class="content">writing-mode takes vertical space</div></div>
<span class="badge">New</span>
<p id="out"></p>
<script>
const size = (id) => { const el = document.getElementById(id); return el.offsetWidth + ' x ' + el.offsetHeight; };
document.getElementById('out').textContent = 'layout box: rotated ' + size('r') + ', writing-mode ' + size('v');
</script>
</body>
</html>
读数显示了每个标签在布局中的尺寸。旋转的那个仍然占据原来又宽又矮的盒子:它旁边留下一块空白,上下还伸出了自己所在的行,压到那里的内容上。使用 writing-mode 的那个则占据一个又高又窄的盒子。再加上 rotate: 180deg,它就会从下往上读,这是竖向标签常见的阅读方向。
点击或悬停时旋转图标
把箭头旋转 180 度,是表示某个区域已展开的标准做法。把 rotate 和过渡效果结合起来:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rotating arrow</title>
<style>
body { font-family: system-ui, sans-serif; }
.toggle { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; font: inherit; border: 1px solid #111827; background: white; border-radius: 6px; }
.chevron { width: 10px; height: 10px; border-inline-end: 2px solid #111827; border-bottom: 2px solid #111827; rotate: 45deg; transition: rotate 0.25s ease-out; }
.toggle[aria-expanded="true"] .chevron { rotate: 225deg; }
</style>
</head>
<body>
<button class="toggle" aria-expanded="false">Details <span class="chevron" aria-hidden="true"></span></button>
<script>
const btn = document.querySelector('.toggle');
btn.addEventListener('click', () => btn.setAttribute('aria-expanded', btn.getAttribute('aria-expanded') !== 'true'));
</script>
</body>
</html>
这个 V 形箭头是一个只有两条边框的正方形,旋转 45 度后指向下方。按钮展开时它转到 225 度,指向上方,过渡效果让这半圈旋转以动画呈现。样式由 aria-expanded 属性驱动,所以视觉状态和屏幕阅读器报告的状态永远不会不一致。
旋转的加载图标
旋转动画就是从 0 度到 360 度的旋转,用 linear 时间函数不断重复,这样速度始终不变:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spinner</title>
<style>
@keyframes spin {
to { rotate: 360deg; }
}
.spinner {
width: 36px;
height: 36px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 3s; } /* slower, still shows activity */
}
</style>
</head>
<body>
<div class="spinner" role="status" aria-label="Loading"></div>
</body>
</html>
只有一个 to 关键帧时,动画从元素自身的旋转角度(没有旋转)开始,到 360deg 结束;infinite 让它不断重复,而且不会跳动,因为 360deg 看起来和 0 一样。对于在系统中要求减少动态效果的用户,加载图标会慢三倍旋转。关键帧的更多内容见动画页面。
3D 旋转:rotateX、rotateY
rotateX() 让元素绕水平轴向后翻转,rotateY() 让元素绕垂直轴转动,就像一扇门。没有透视时,它们看起来只是被压扁了;在父元素上设置 perspective 可以增加纵深感:
.scene { perspective: 600px; }
.card { transform: rotateY(35deg); }
.flip { transform: rotateY(180deg); backface-visibility: hidden; } /* card flip */
如果想镜像翻转元素而不是旋转它,请使用 scale: -1 1(水平翻转)。旋转 180 度会把元素上下颠倒,这和镜像翻转不是一回事。
常见错误
- 旋转
<span> 或 <a> 却纹丝不动。 普通的行内元素会忽略变换。加上 display: inline-block。
- 悬停时旋转消失了。 带有
transform: scale(1.1) 的悬停规则会替换掉 transform: rotate(...)。使用单独的 rotate 和 scale 属性,或者把两个函数都写上。
- 以为布局会自动腾出空间。 旋转后的图片仍然保留未旋转时的盒子,并会盖住相邻元素。给容器留出旋转后的尺寸,或者对文字使用
writing-mode。
- 加载图标使用
ease。 默认的时间函数在每一圈都会先加速再减速。请使用 linear。
- 绕错了旋转点。 先设置好
transform-origin,再调整角度。