列表动画我配了三年,今天发现 CSS 自己会算「第几个」了——sibling-index() + @starting-style 把入场动画全删了

以前给列表做入场动画,每一项都要用 JS 算「第几个」,然后塞进 style 属性。今天 CSS 自己会算了——sibling-index() 把这件事彻底还给了浏览器。


以前怎么做的

做一个卡片列表,每项依次淡入上移,传统的做法是这样的:

<ul class="card-list">
  <li class="card" style="--i: 0">卡片 1</li>
  <li class="card" style="--i: 1">卡片 2</li>
  <li class="card" style="--i: 2">卡片 3</li>
</ul>
.card {
  animation: slide-up 0.5s both;
  animation-delay: calc(var(--i) * 0.1s);
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

JS 要么手写循环塞 --i,要么用模板引擎生成。问题是:中间插一项,后面的 index 全要重新算;列表是动态加载的,还要监听 DOM 变化重新计算。


现在 CSS 自己会了

sibling-index() 是 CSS 2026 新函数,返回当前元素在父容器中的序号(从 1 开始)。Chrome 129+ Baseline。结合 @starting-style——这个规则让 CSS 在元素首次渲染时就能触发动画,而不需要 JS 事件触发。

完整方案:

.card-list {
  /* 容器声明时间轴作用域 */
  timeline-scope: --card-timeline;
}

.card {
  animation: slide-up 0.5s both;
  /* 直接拿序号算延迟,浏览器自动完成 */
  animation-delay: calc((sibling-index() - 1) * 0.1s);
  animation-range: entry 0% entry 100%;
}

@keyframes slide-up {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
}

@starting-style {
  .card {
    animation-name: slide-up;
  }
}

不需要任何 JS,不需要给每个 <li>--i。浏览器自己知道「这是第几个」。


进阶:sibling-count() 配合比例分布

sibling-index() 搭配 sibling-count()(当前容器总子项数),可以做比例分布的动画——越靠后的元素延迟越短或越长:

.card {
  /* 序号 / 总数,得到 0~1 的进度值,再乘以总时长 */
  animation-delay: calc(
    (sibling-index() / sibling-count()) * 0.8s
  );
  animation-duration: 0.6s;
  animation-fill-mode: both;
}

这样不管列表有多少项,动画分布始终均匀,不会因为列表变长导致最后一项目动画延迟过长。


动态列表也不怕

对于动态加载的列表,传统方案需要在每次插入新项后重新遍历计算 index,sibling-index() 不需要:

// 以前:插入新项后要手动更新所有后续项的 --i
const list = document.querySelector(".card-list");
const newCard = document.createElement("li");
newCard.className = "card";
newCard.textContent = "新卡片";
list.appendChild(newCard);
// 如果用 JS 算 index,这里需要遍历后面每一项重新赋值

// 现在:直接 append,CSS 自动重新计算
list.appendChild(newCard); // 动画自动错开,不需要额外处理

浏览器在渲染时会重新计算 sibling-index() 的值,动态增删列表项完全不影响动画的正确性。


降级处理

sibling-index() 目前只有 Chrome 129+ 支持,需要降级:

.card {
  /* 基础动画,无错开 */
  animation: slide-up 0.5s both;
}

@supports (animation-timeline: scroll()) {
  .card {
    /* 增强:有错开的入场动画 */
    animation-delay: calc((sibling-index() - 1) * 0.1s);
    animation-range: entry 0% entry 100%;
  }
}

不支持的浏览器会使用基础动画,体验平稳退化,不报错不白屏。


一个完整可运行的例子

<!DOCTYPE html>
<html lang="zh">
<head>
<style>
.card-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  timeline-scope: --card-timeline;
}

.card {
  padding: 16px 20px;
  background: #f5f5f5;
  border-radius: 8px;
  font-family: system-ui, sans-serif;
  font-size: 15px;
  color: #333;

  /* 入场动画 */
  animation: slide-up 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc((sibling-index() - 1) * 80ms);
  animation-range: entry 0% entry 100%;
}

@keyframes slide-up {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}

@starting-style {
  .card {
    animation-name: slide-up;
  }
}
</style>
</head>
<body>
<ul class="card-list">
  <li class="card">第一项 — 立即出现</li>
  <li class="card">第二项 — 延迟 80ms</li>
  <li class="card">第三项 — 延迟 160ms</li>
  <li class="card">第四项 — 延迟 240ms</li>
  <li class="card">第五项 — 延迟 320ms</li>
</ul>
</body>
</html>

打开 Chrome 129+,刷新页面,五张卡片依次入场,体验流畅。插一项进去,后面的自动重新排。


下一步怎么做

如果你的项目里列表入场动画还在靠 JS 算 index,可以分两步迁移:

  1. 先把 @starting-style 跑起来,让元素首次渲染能触发动画,不需要任何 JS 触发
  2. 把手写的 --i 变量删掉,换成 sibling-index(),浏览器自己会算

对于不支持的浏览器(比如 Safari、Firefox),用 @supports 包一层降级,先保证能用,再逐步增强。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 702 阅读