CSS锚点定位和滚动驱动动画,怎么让脚注像弹窗一样在屏幕边缘弹出?

2,956字
13–19 分钟
in

写博客的时候总想塞点额外的小贴士或者吐槽,放正文里怕打断阅读节奏,搁底下又不够显眼。要是能让脚注像聊天里的气泡弹窗一样,自动飘到文章两侧的空地儿上,并且随着页面滚动一个个蹦出来,那得多带劲。这事儿搁以前得写一堆JavaScript监听滚动和计算位置,现在靠CSS的锚点定位加滚动驱动动画就能整得明明白白。

目录

先看个大概效果:正文里的每个脚注标签(比如一段话末尾的小注释)会脱离原本的位置,飞到屏幕左右两侧固定区域,并且当这个脚注块滚动到可视区域时,它会有一个从小到大、从透明到可见的弹出动画。屏幕太窄的时候(比如手机),脚注自动变回普通内联样式,不占两侧空间。

核心概念

锚点定位(CSS Anchor Positioning)是一套让绝对定位元素相对于另一个元素(叫“锚点”)来摆位置的机制。传统绝对定位只能相对于最近的具有定位属性的祖先容器,而锚点定位可以绑定页面里任意一个元素。滚动驱动动画(Scroll-Driven Animations)则把动画的进度条从“时间”换成了“滚动位置”或“元素可见比例”,想实现“元素出现一半就弹出来”这种效果简直不要太爽。

实战流程

准备标记

HTML结构走起。假设有一篇博客文章,里面某些段落末尾塞了一个脚注容器。

<main class="post">
  <h1>标题</h1>
  <p>正文第一段,讲了个超有趣的冷知识<span class="footnote">冷知识出处:某本古老秘籍第42页</span>。</p>
  <p class="note">另一个段落,这里写重要观点<span class="footnote">观点补充:实际上还有第三种可能性</span>。</p>
  <p class="note">再一个段落,继续唠<span class="footnote">唠嗑脚注:这玩意儿真能跑起来</span>。</p>
</main>

注意.footnote就是那个要飞出去的目标元素,它原本乖乖待在段落末尾。.post是整个文章容器,后面会把它注册成锚点。

锚点设置

.post起一个锚点名,用anchor-name属性,值必须是--开头的自定义标识符。

.post {
  anchor-name: --post;
}

这一步就像在地上钉了个钉子,之后所有脚注都能拿这个钉子当参照物。要是页面里有多篇文章,每篇各自的.post可以起不同的锚点名,互不干扰。

目标定位

.footnote变成绝对定位元素,然后绑定到锚点--post上。用position-anchor声明默认锚点,再用anchor()函数读取锚点的某条边位置。

.footnote {
  position: absolute;
  position-anchor: --post;
  background: #fff;
  border-radius: 20px;
  padding: 20px;
  margin: 0 20px;
  max-width: 200px;
}

现在脚注已经脱离文档流,但它们全挤在文章右侧边缘,因为还没指定具体放在左边还是右边。用nth-of-type选择器让奇数脚注贴右侧,偶数脚注贴左侧。由于.footnote不是.note的直接子元素?实际结构里每个.footnote都在各自.note内部,而.note.post下的同级段落。要按段落顺序交替放置,应该基于.note来计数。

.note:nth-of-type(odd) .footnote {
  left: anchor(right);
}

.note:nth-of-type(even) .footnote {
  right: anchor(left);
}

奇数段落里的脚注,让它的left值等于锚点--post的右边缘位置,脚注就贴在文章框的右侧外面。偶数段落里的脚注,right值等于锚点的左边缘位置,脚注就贴在左侧外面。margin会在外侧撑开一段空白,避免紧贴边缘。

写代码时容易犯一个错:把nth-of-type写成nth-child。如果段落之间混了图片、引用块之类的元素,nth-child计数会乱套,而nth-of-type只数.note元素,稳得很。

弹出动画

弹出动画用@keyframes定义,让脚注从半透明缩小状态变成完全不透明。

@keyframes pop-up {
  from {
    opacity: 0;
    transform: scale(0.5);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

把动画绑定到.footnote上,但动画时长不设固定秒数,而是交给animation-timeline: view()来控制。这个view()关键字让动画进度随着元素进入视口的程度走。

.footnote {
  /* 继承上面的定位样式 */
  animation: pop-up linear;
  animation-timeline: view();
  animation-range: cover 0% cover 40%;
}

animation-range设置动画的起止点:当脚注刚刚露出一点点(0%覆盖)时开始播放,当它露出四成(40%覆盖)时就播完。这样脚注还没完全滚到屏幕中间就已经完成弹出效果,看着更跟手。如果去掉animation-range,默认是元素完全进入视口才开始,离开视口才结束,体验差一截。

移动适配

窄屏幕下左右两侧根本没空间放飞出去的脚注。解决方法是用媒体查询,宽度小于1000px时把脚注恢复成普通块级元素,不再绝对定位。

@media (width <= 1000px) {
  .footnote {
    position: static;
    margin: 10px 0 0 0;
    display: flex;
    gap: 10px;
    background: #fce6c2;
    border-radius: 12px;
  }
  .footnote::before {
    content: "📌 注:";
    font-weight: bold;
  }
  /* 重置那些left/right和anchor相关属性,避免冲突 */
  .note:nth-of-type(odd) .footnote,
  .note:nth-of-type(even) .footnote {
    left: auto;
    right: auto;
  }
}

position: static让脚注回到正常文档流里,老老实实跟在段落屁股后面。加个伪元素::before显示“注:”字样,在手机上看着更明白。媒体查询的阈值设1000px,因为一般博客正文宽度约700-800px,两侧留出200px以上才放得下气泡。

还有一个细节:如果同时用left: anchor(right)right: anchor(left),两个声明写在不同选择器里不会互相覆盖,但注意在窄屏重置时要把它们都干掉。上面的代码用了.note:nth-of-type(odd) .footnote, .note:nth-of-type(even) .footnote一次性重置。

整活儿完成。跑一下效果:桌面浏览器里,滚动页面时脚注会从两侧pop出来,动画顺滑;手机上看,脚注变成卡片样式跟在正文后面,不占两侧空间。整个实现没有一行JavaScript,全是CSS的新特性在撑场面。锚点定位和滚动驱动动画这两个玩意儿单独用已经很香,搅和在一起能搞出更多花活——比如侧边栏目录随滚动高亮、阅读进度条、视差弹出层等等。