固定输入框里一打字,页面就往上跳:scroll-padding 与焦点滚动
搜索结果页往下滚了一段,顶部搜索框仍然可见。点进去补几个字,下面的结果却跟着往上跳;继续输入,页面会一步步退回顶部。
这个问题出现在一个 Nuxt 搜索页面里。起初值得怀疑的地方不少:输入会更新字符计数,筛选条件显示成标签,Header 使用 sticky,页面还接了 OverlayScrollbars。测量以后发现,搜索框的位置和尺寸一直没变,变的是文档的 scrollY。
最终修复只有一行:删除根元素上的 scroll-padding-top: 166px。问题来自文档级滚动留白与 sticky 输入框的光标滚动互相影响。其他页面若确实需要正文定位,可以把偏移配置在正文目标上。
留白改变的是滚动目标区域
scroll-padding-top 容易被当成「锚点跳转时给导航栏让一点位置」。目录定位只是它的一种用途。
CSS Scroll Snap 规范的 scroll-padding 定义说的是滚动容器的 optimal viewing region,也就是把目标带入视野时使用的区域。它适用于普通滚动容器,即使没有开启滚动吸附也会参与;设置在根元素上时,会应用到文档视口。
这里的「留白」不会给盒子加内边距,也不会改变元素实际是否可见。它会参与判断元素或光标是否已经滚入目标区域,包括聚焦时的滚动。
这就解释了两个看起来矛盾的事实:输入框明明能看见,浏览器仍可能尝试滚动页面;输入框没有发生布局变化,列表仍然会移动。
触发问题的结构可以缩成下面这段等价 CSS:
/* 文档视口顶部留出 166px,但输入框本身也停留在这段区域里。 */
:root {
scroll-padding-top: 166px;
}
.search-header {
position: sticky;
top: 0;
}
滚动到 Header 吸顶以后,输入框仍然停在视口顶部。按键引起光标位置更新,浏览器尝试把光标放进留白以外的目标区域,于是改变文档滚动位置。可是 Header 仍被 sticky 固定住,光标在视口里的位置没有随正文一起改变。下一个按键又会碰到同样的条件。
这里的固定指 sticky 吸顶。上面的解释对应本次验证的结构与浏览器表现,不把每次滚动的距离当成规范规定的算法。
Chromium issue 374034263记录过相同组合:根元素有 scroll-padding-top,sticky 区域内有可聚焦控件,按键会让页面向上滚动。记录中的确认意见也把原因指向了按键触发的 scroll-into-view。规范说明属性会影响光标滚动,缺陷记录和本机实验则说明这个组合具体怎样失效。
用原生页面做一次对照
为了排除业务状态和第三方库的影响,我把场景缩成原生 HTML、CSS 与 JS:一个 sticky 搜索框、40 条静态资料、一个切换根元素滚动留白的复选框。页面只读取 scrollY,输入事件不会写回滚动位置。
在 Windows、Chrome 155.0.8059.85、1280 × 800 CSS 像素视口下,先滚到 1100px,再点击输入框并逐字输入,得到下面这组数据。两组之间只改变根元素的滚动留白。
| 动作 | 根元素留白 166px | 去掉根元素留白 | 输入框 top |
|---|---|---|---|
| 滚动后 | 1100 | 1100 | 41px |
| 点击聚焦后 | 680 | 1100 | 41px |
| 输入第一个字 | 567 | 1100 | 41px |
| 输入第二个字 | 454 | 1100 | 41px |
| 输入第六个字 | 2 | 1100 | 41px |
中间两列是文档的 scrollY,最后一列是输入框相对视口的 getBoundingClientRect().top。留白开启时,连续按键每次让文档上移约 113px,输入框却一直停在 41px。原项目里的输入框位置不同,测到的跳动幅度约为 127px;这些都是特定布局下的实测值。
下面是最小复现页连续输入六个字符后的真实截图。开启全局留白时,原本滚到 1100px 的列表已接近顶部,右下角读数为 2。

去掉根元素留白后,执行同样的操作,列表仍停留在原来的位置,读数保持 1100。

390px 宽度下也做了同样的对照。这里模拟的是手机宽度,没有覆盖真实手机软键盘,也没有据此宣布 Safari、Firefox 或所有 WebView 都有相同表现。完整阶段数据保存在浏览器观察记录中。
可以在下面的预览里切换配置。先在预览内部向下滚,再输入文字;如果当前浏览器不再复现,以实际读数为准。
读取 /files/sticky-input-scroll-padding-focus-jump/kits/sticky-input-scroll-guard/ 的 README.md、MANIFEST.json 和 FILES.json。
按索引查看复现页、Stylelint 配置和浏览器检查,将滚动位置断言迁移到项目真实入口。
修复时把滚动偏移放回需要它的地方
这次搜索页没有需要文档级统一偏移的锚点流程。因此,直接从全局 Stylus 中删除这一项,保留原来的 Header 与原生输入行为:
:root
- scroll-padding-top 166px
如果正文确实需要定位偏移,可以把需求表达在正文目标上。例如,目录链接会跳到文章标题,就只给这些标题设置 scroll-margin-block-start:
/* 只影响正文标题作为滚动目标时的定位区域。 */
.article-heading[id] {
scroll-margin-block-start: 166px;
}
这个属性放在被定位的目标上,不会自动改变 sticky 搜索框的滚动区域。偏移值仍要按实际 Header 高度与断点设计;不能把这里的 166px 当成通用尺寸。规范对 scroll-margin 的定义也说明了它怎样影响目标的滚动定位范围。
另一类页面本来就有独立的内容滚动容器:搜索区在容器外,正文在容器内。此时可以把 scroll-padding 放在那个内容容器上。普通文档不必为了避开这一个问题,就额外引入一套嵌套滚动。
这些替代写法适用于确有定位需求的页面,不是这次应用修复额外引入的代码。修改后仍要检查目录跳转、Tab 焦点和正文目标是否被头部遮挡。
只给 focus 加 preventScroll 还不够
input.focus({ preventScroll: true }) 能约束这一次程序聚焦,但随后按键引起的光标滚动仍然可能发生。在同一最小复现里,保留全局留白并这样聚焦,结果是:
[
{ "stage": "滚动后", "scrollY": 1100 },
{ "stage": "聚焦后", "scrollY": 1100 },
{ "stage": "输入 1", "scrollY": 987 },
{ "stage": "输入 2", "scrollY": 874 }
]
这段是观察记录中的桌面场景节选,只保留阶段和 scrollY。聚焦时没有跳,输入时仍然跳。因此,给现有 focus() 补一个选项,并没有处理这次冲突的来源。
同样不适合用每次输入后写回旧 scrollTop 来处理:那会让应用和浏览器各自修改滚动位置,还需要处理选区、输入法和软键盘。这个方案没有用于本次修复。
静态告警提醒复核,浏览器检查负责确认
这个问题能沉淀成检查规则,但静态工具掌握的信息有限。scroll-padding 是合法能力:只有正文、导航栏没有输入框的文章页,就可能需要全局留白。仅凭一段 CSS,很难知道最终哪个控件吸顶、光标在哪里、变量解析成多少像素。
因此,这里提供的是选择加入的项目策略:对明确的 html、:root 声明产生 warning,提醒开发者复核固定输入框。它复用 Stylelint 内置的 rule-selector-property-disallowed-list,不需要再写一个同名职责的插件规则。
源码包里的配置同时覆盖独立根选择器和 :root, .theme 这类选择器列表,检查属性包括简写及逻辑属性:
// 合并进现有 Stylelint rules;完整配置见上面的源码工作区。
{
'rule-selector-property-disallowed-list': [
{ '/(?:^|,)\\s*(?:html|:root)\\s*(?=,|$)/i': ['/^scroll-padding(?:-|$)/'] },
{ severity: 'warning' },
],
}
这条策略只按选择器和属性判断,scroll-padding-top: 0 也会提醒。局部内容容器和正文的 scroll-margin 不受它限制;html[data-theme]、嵌套别名、JS 动态样式则属于当前配置的漏检边界。Vue SFC、Stylus、SCSS 等文件还需要项目现有的解析或编译流程。不能把 warning 当成已确认的浏览器缺陷,也不能把没有 warning 当成稳定性证明。
已经核实的合法用法,可以用带原因的局部禁用注释保留;这条检查不提供自动删除样式的修复。源码包包含 10 个配置用例,分别验证应提醒与应放过的范围。
运行时的验证则应保留一个直接的不变量:用户只是在固定输入框中编辑时,原先的正文滚动位置保持不变。
不能只在首屏输入,也不能只用 fill() 一次性替换值。本次检查先让页面产生足够长的内容,向下滚动,再点击聚焦、逐字输入、清空、粘贴和中间插入,每一步读取真实 scrollY。真实应用还验证了快捷键聚焦与条件标签删除,在 1100px 和 2200px 位置都保持稳定。输入法组合阶段、真实软键盘和其他浏览器,继续按目标产品的兼容范围补测。
总结
这次输入跳动时,搜索框的尺寸和视口位置没有变化,浏览器却因根元素滚动留白与 sticky 光标定位的冲突反复滚动文档。去掉不需要的全局 scroll-padding-top 后,原生对照页和真实应用中的滚动位置恢复稳定;仅限制一次 focus(),不能阻止后续按键触发的光标滚动。
固定 Header 的视觉稳定与文档滚动稳定要分别检查。遇到类似跳动,先同时量 DOMRect 与真实 scrollTop,再核对全局滚动留白、焦点和光标的定位范围;确有正文定位需求时,再按目标或内容容器配置偏移。静态 warning 只负责提示复核,输入法、真实软键盘和其他浏览器仍需按产品范围补测。