弹层组件最烦的两件事——尺寸跟着锚点走、滚动时消失——今天 CSS 自己全包了

弹层组件最烦的两件事——尺寸跟着锚点走、滚动时消失——今天 CSS 自己全包了

配弹层组件最烦的从来不是定位,是两件事绑在一起:尺寸要跟着锚点走,页面一滚动弹层得消失。我以前配这个要一套 JS 算坐标,再加一套事件监听判断滚动方向。CSS 锚点定位把定位这件事原生化了,但尺寸和滚动行为还在靠 JS。今天 anchor-size() 和 scroll-driven animations 组合起来,让我把最后那套 JS 全删了。


anchor-size():弹层宽高直接引用锚点的

CSS Anchor Positioning 里,弹层通过 anchor-name 绑定到锚点元素,然后用 anchor() 系列函数读取锚点的位置和尺寸。

anchor-size() 是专门用来读锚点尺寸的:

/* 弹层宽度等于锚点宽度 */
width: anchor-size(width);

/* 弹层高度等于锚点的高度 */
height: anchor-size(height);

/* 用命名锚点,明确指定 */
width: anchor-size(--my-trigger width);
height: anchor-size(--my-trigger height, 200px); /* 带兜底值 */

/* 也可以用 block/inline/self-block/self-inline */
block-size: anchor-size(block);
inline-size: anchor-size(self-inline);

anchor-size() 接受两个参数:

  • 锚点名(可选,默认用隐式锚点)
  • 尺寸维度:width / height / block / inline / self-block / self-inline

关键点:这个函数返回的是长度值,可以直接参与 calc() 运算。比如弹层要比锚点宽 20px、高 10px:

width: calc(anchor-size(width) + 20px);
height: calc(anchor-size(height) + 10px);

配合 @position-try 的 fallback 机制,弹层找不到合适位置时可以自动换边:

@position-try {
  top: anchor(--my-trigger top);
  left: anchor(--my-trigger left);
}
width: anchor-size(width);
height: auto;

scroll-driven animations:让弹层「跟着滚」消失

CSS Scroll-Driven Animations 把滚动位置变成了时间轴。结合 animation-timeline: scroll(),让弹层在滚动时自动隐藏:

@keyframes hide-on-scroll {
  from { opacity: 1; visibility: visible; }
  to   { opacity: 0; visibility: hidden; }
}

.popover {
  animation: hide-on-scroll linear;
  animation-timeline: scroll();
  /* 或者指定滚动容器 */
  animation-timeline: scroll(root block);
}

scroll-state() 提供了四种状态值——scrolled(已滚动)、stuck(被粘住)、snapped(吸附中)、scrollable(可滚动)。结合 animation-timeline: scroll-state(scrolled) 可以更精确地控制在滚动发生后才触发:

@keyframes popover-fade {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-8px); }
}

.popover {
  animation: popover-fade ease-out;
  animation-timeline: scroll-state(scrolled);
}

把两件事合起来:一个纯 CSS 弹层

下面是完整的组合示例——弹层尺寸引用锚点,滚动时自动淡出消失,不需要一行 JS:

<button class="trigger" style="anchor-name: --my-trigger">点击展开</button>
<div class="popover" popover>
  <p>弹层内容,宽度等于按钮宽度</p>
</div>
/* 锚点绑定 */
.popover {
  position-anchor: --my-trigger;

  /* anchor-size() 让弹层宽度跟随锚点 */
  width: anchor-size(width);
  min-height: anchor-size(height);

  /* @position-try 找不到位置时自动换边 */
  @position-try {
    top: anchor(--my-trigger bottom);
    left: anchor(--my-trigger left);
  }

  /* 滚动时淡出消失 */
  animation: popover-fade-out ease-out both;
  animation-timeline: scroll();
}

@keyframes popover-fade-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-4px) scale(0.97); }
}

实际效果:按钮尺寸变了,弹层自动跟着变宽;页面开始滚动,弹层优雅地淡出消失。


三个实战注意点

1. anchor-size() 有浏览器限制

目前只有 Chromium 内核支持(Chrome 115+),Firefox 和 Safari 还没上。生产环境用之前记得加 @supports 渐进增强:

.popover {
  @supports (width: anchor-size(width)) {
    width: anchor-size(width);
  }
  /* 兜底:不给固定宽度让它自适应内容 */
  width: max-content;
}

2. scroll-driven animations 的 timeline 作用域

animation-timeline: scroll() 默认找最近的滚动容器。如果页面是单滚动容器,用 scroll(root block) 明确指定;如果是局部滚动,要确保弹层在滚动容器内部或者单独处理。

3. anchor-name 不要冲突

每个 anchor-name 只能绑定一个锚点。如果页面有多个弹层,要用不同的命名:

.trigger-primary { anchor-name: --popover-1; }
.trigger-secondary { anchor-name: --popover-2; }

下一步

如果你的项目还在用 JS 算弹层坐标,今天可以把锚点定位先接进来,anchor()anchor-size() 可以单独用,不需要等 scroll-driven animations 全部 Baseline。把定位和尺寸这两件事先收了,滚动行为作为渐进增强项单独处理。查一下 Can I Use 的 anchor-size 覆盖率,等到 Firefox 支持的时候,这套方案的完整版就可以上线了。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 155 阅读