返回博客

CSS transition 进退场用同一个 class 写——进入顺滑、退出拖尾的根因

做轮播卡片的 active 放大效果时,UI 同学说:"进入 active 挺好的,但切走的时候那个缩回去太慢了,感觉黏着的。"改小 duration?进入又变得太仓促。调 easing?两边一起变。折腾半天发现根本改不了——因为压根不知道 CSS transition 是"从哪里读配置的"。

现象

给卡片写了一个 active 状态切换,base class 挂 transition: transform 0.35s ease

.card {
  transform: scale(0.92);
  opacity: 0.7;
  transition: transform 0.35s ease, opacity 0.35s ease;
}
.card--active {
  transform: scale(1);
  opacity: 1;
}

进退场都是 0.35s,没办法单独调某个方向。想让进入快一点(0.15s)、退出保持余韵(0.35s),无论怎么改 .card 上那一行,两个方向都跟着变。

根因

CSS transition 的工作机制有一个经常被忽视的细节:

transition 规则从目标态(target state)的 computed style 读取,而不是起始态。

具体到 class 切换:

  • 进入 .card--active(加 class):浏览器读 .card--active 的 computed style → transition 规则取自 .card--active 上的声明
  • 退出 .card--active(移除 class):浏览器读 .card(base)的 computed style → transition 规则取自 .card 上的声明

这意味着,transition 声明写在哪个 class 上,就只对"进入那个 class"这个方向生效。

坑:把 transition 只写在 base class,两个方向都跟着动

transition 写在 .card 上,退出 active 时读 .card,进入 active 时也读 .card(因为 .card--active 没有声明,回落到 base)——结果进退场用同一份配置,想单独改某个方向根本做不到。

正解

transition 拆开:进入方向的配置挂在 .card--active,退出方向的配置挂在 .card(base):

Pattern A — 进缓退即时(进入平滑,退出瞬时硬切):

.card {
  transform: scale(0.92);
  opacity: 0.7;
  /* base 不写 transition → 退出 active 时瞬间收回 */
}
.card--active {
  transform: scale(1);
  opacity: 1;
  transition: transform 0.15s ease, opacity 0.15s ease;
  /* 只在 active 上写 → 进入时平滑 0.15s */
}

Pattern B — 进快退慢(进入锐利,退出保留余韵):

.card {
  transform: scale(0.92);
  opacity: 0.7;
  transition: transform 0.35s ease, opacity 0.35s ease; /* 退出时读这里 */
}
.card--active {
  transform: scale(1);
  opacity: 1;
  transition: transform 0.15s ease, opacity 0.15s ease; /* 进入时读这里 */
}

Pattern C — 进退场不同曲线(进入有弹性 overshoot,退出平滑):

.card {
  transform: scale(1);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* exit */
}
.card--active {
  transform: scale(1.1);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); /* bouncy enter */
}
正解:transition 分离写到 base 和 active,进退场各自配速

进入 active 读 .card--active 的 transition,退出 active 读 .card 的 transition。两份声明互不干扰,duration 和 easing 可以完全独立。React / Vue 的 className 切换最终也是 DOM class change,机制完全一致。

排查过程回放

这个坑的排查经历了四个版本:

  1. 只在 base 挂 0.35s → 进退都 0.35s,进入感觉拖沓
  2. 把 base 改成 0.15s → 进退都变快,退出太突然,用户反馈"感觉被切断"
  3. base 删掉 transition,只在 active 挂 0.15s → 进入 OK,退出变成硬切,视觉余韵全没
  4. base 挂 0.35s + active 挂 0.15s → 进退分离,符合预期

如果一开始知道"transition 从目标态读"这个机制,第四版是直觉答案,不需要试错。

一句话外卖

CSS transition 写在哪个 class 上,就只对"进入那个 class"的方向生效——拆到 base 和 active 两个 class,进退场就能独立配速。