CSS 教程 / CSS 变换与过渡:顺序、原点与交互状态

变换会移动、旋转或缩放盒子的渲染外观,而不会重新布局周围内容。过渡会在属性值变化之间添加动画,因此要有意识地选择属性和时间,并提供简化动画的替代方案。

理解变换与正常流

文档流指的是元素最初放置的位置与周围元素之间的关系。即使你将按钮移动到translateY(-.5rem),下方段落也不会根据按钮移动后的位置向上移动。变换后的元素可能会与周围内容重叠或超出屏幕,因此在原始位置留出足够的空间比增加移动距离更为重要。

函数 示例 观察结果
translate() translate(1rem, -.5rem) 向右移动1rem,向上移动0.5rem
rotate() rotate(15deg) 围绕参考点旋转
scale() scale(1.05) 显示为原始大小的1.05倍

仅仅因为尺寸看起来更大,并不意味着变换前盒子占用的空间会更大。请查看网格或宽度属性以更改卡片的实际列宽,或者使用变换以在悬停或聚焦时创建稍大一点的视觉效果。未被替换的行内盒子,例如纯文本 span,不能直接应用 transform,因此如有必要请指定 display: inline-block。有关变换目标和坐标处理的定义,请参见 MDN:transform 。

比较变换顺序和原点

当你列出变换函数时,每个函数都会影响后续函数所用的坐标系。这是在描述组合变换的坐标关系,不是依次播放两段动画。因此,平移/旋转和旋转/平移通常不会得到相同的结果。在下面的示例中,A 的旋转后的盒子在向右移动的位置可见,而 B 的盒子沿受旋转影响的方向移动,并且进一步向下可见。不要仅仅看函数名称就假设两个结果相同;要比较它们的实际位置。

旋转和扩展的中心设置为 transform-origin。如果使用 left top 而不是默认中心,它将围绕左上角旋转。当你想固定特定点时,这很有用,例如当菜单打开时,但只需改变参考点,周围内容的重叠可能会发生变化。

将以下代码保存为 UTF-8 编码的 transform-compare.html,并在浏览器中打开。无需外部文件或库。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Transform order and origin</title>
  <style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; color: #183044; font-family: sans-serif; line-height: 1.6; }
main { max-width: 46rem; margin-inline: auto; }
h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
button, input { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
.comparisons { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); gap: 1rem; }
.slot { padding: 1rem; border: 1px solid #8a9ba7; }
.frame { height: 9rem; display: grid; place-items: center; background: #f0f2f4; }
.box { width: 4rem; height: 2rem; display: grid; place-items: center; background: #166b92; color: white; }
.first { transform: translateX(2rem) rotate(45deg); }
.second { transform: rotate(45deg) translateX(2rem); }
.center { transform: rotate(30deg); transform-origin: center; }
.corner { transform: rotate(30deg); transform-origin: left top; }
  </style>
</head>
<body>
<main>
<h1>The same value also varies depending on the order and reference point</h1>
<div class="comparisons">
  <section class="slot"><h2>Translate, then rotate</h2><div class="frame"><div class="box first">A</div></div></section>
  <section class="slot"><h2>Rotate, then translate</h2><div class="frame"><div class="box second">B</div></div></section>
  <section class="slot"><h2>Center origin</h2><div class="frame"><div class="box center">C</div></div></section>
  <section class="slot"><h2>Top-left origin</h2><div class="frame"><div class="box corner">D</div></div></section>
</div>
</main>
</body>
</html>
  1. 比较 A 和 B 的位置。如果将 .second 的函数顺序更改为与 A 相同,两个盒子相对于各自 .frame 的位置就会相同。确认后,将其恢复到原始顺序。
  2. C 和 D 的旋转角度相同。在 .corner 规则中仅将 transform-origin 改为 center,检查参考点的差异,然后恢复到 left top。
  3. 通过仅更改 C 的旋转为 scale(1.2) 来比较放大效果。盒子外的区域会增大,但周围区域不会重新定位。确认后,将其恢复到 rotate(30deg)。

选择过渡属性与时间

transition 本身不会改变状态。只有在悬停、聚焦或者类名改变时 CSS 值变化时才会开始变化。指定要影响的属性名称、持续时间、进展曲线和启动延迟;如果延迟省略,则默认为 0 秒。

transition: transform 180ms ease 0ms, background-color 180ms ease 0ms;

这里,180ms 为 0.18 秒,而 ease 是一个平滑连接开始和结束的曲线,而不是仅以相同速度进行。linear 以恒定速率变化。先使用较短的时间且不添加延迟,然后根据需要进行调整。给看似无响应的按钮添加长延迟可能会使你难以注意到操作的结果。

将 transition 放在基础规则上可以让它在进入和返回时都能操作。仅在 :hover 上声明的过渡在悬停结束时会消失,并可能产生不同的返回效果。应有意命名属性,而不是使用 all。此示例对 transform 和 background-color 进行过渡;不是每个属性的插值方式都相同。有关更多详情,请参阅 MDN:transition。

测试悬停和键盘聚焦

将下面的代码保存为 transition-state.html,以 UTF-8 编码保存并在浏览器中打开。当你将鼠标悬停在按钮上或使用 Tab 键时,按钮会向上移动并且背景颜色会改变。获得键盘焦点时还会显示深蓝色轮廓。按钮仅用于练习以观察状态变化,按下按钮后不会执行额外的操作。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Transforms and state transitions exercise</title>
  <style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; color: #183044; font-family: sans-serif; line-height: 1.6; }
main { max-width: 46rem; margin-inline: auto; }
h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
button, input { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
.stage { padding: 2rem 1rem; border: 2px dashed #8a9ba7; }
.action { padding: .75rem 1.25rem; border: 1px solid #166b92; border-radius: .4rem;
  color: #183044; background: #eaf4fa; transform: translateY(0);
  transition: transform 180ms ease, background-color 180ms ease; }
.action:hover, .action:focus-visible { transform: translateY(-.5rem); background: #cfe8f4; }
.neighbor { margin-block: 1rem 0; padding: .75rem; background: #f0f2f4; }
@media (prefers-reduced-motion: reduce) {
  .action { transition: none; }
  .action:hover, .action:focus-visible { transform: none; }
}
  </style>
</head>
<body>
<main>
<h1>The paragraph stays in place as the button moves</h1>
<p>Hover over the button or press Tab to focus it.</p>
<div class="stage">
  <button class="action" type="button">Check state</button>
  <p class="neighbor">This paragraph stays in place when the button is transformed.</p>
</div>
</main>
</body>
</html>
  1. 记住下面段落的顶部位置并悬停在按钮上。只有按钮会移动,段落必须保持在原位。
  2. 将 180ms 中的两个地方的持续时间更改为 800ms 以观察进入和返回,然后返回原始值。
  3. 保持时间不变,仅将进度曲线更改为 linear 以进行比较。然后恢复为 ease。
  4. 我们只在第一个过渡条目末尾添加 400ms 延迟:只有 transform 推迟开始,background-color 仍立即开始。比较后删除延迟。
  5. 使用 Tab 键聚焦按钮,用 Shift+Tab 退出。确保焦点轮廓和背景颜色能描述操作状态。

减少运动,并检查副作用

如果用户设置了减少动画,prefers-reduced-motion: reduce 条件匹配时,本例在该条件内声明的 CSS 会取消位移和过渡,只保留背景颜色和焦点标记。该媒体特性只检测偏好,具体如何减少动态效果由 CSS 规则决定。如果你只是移除过渡,变化会立即发生,但你移动到的最终位置将保持不变。在上面的示例中,我们决定装饰性的动画本身是没有必要的,所以即使是悬停和聚焦的变换也被改为 none。请参考 MDN:prefers-reduced-motion。

通过更改操作系统设置或在浏览器开发者工具的渲染面板中重现媒体特性来比较这两种状态。在缩减设置下,按钮不应移动,仅颜色和焦点指示器应改变。完成实验后,将仿真恢复到默认状态。

一个非 none 的 transform 会创建一个堆叠上下文,并可以为绝对定位和固定定位的子元素建立包含块。如果弹出窗口或固定元素表现异常,请检查已变换的祖先元素。同时检查重叠、键盘焦点和缩减运动;仅平滑过渡并不能建立可用布局。参见 CSS 位置:relative、absolute、fixed 和 sticky,CSS z-index 和堆叠上下文。