当对父元素进行缩放时,CSS定位属性sticky会产生奇怪的行为,粘性滚动的速度与元素不同。

6

我不敢相信之前没有人注意到这个问题,但似乎没有人注意到这种行为。首先我应该说明这个html是很久以前写的(不是我写的),目前不能修改。

所以问题来了:

我们有这样结构化布局的html:

.rptDisplay {
  overflow: auto;
  position: relative;
  top: 0;
  left: 0;
  z-index: 0;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 200px;
  max-height: 100vh;
  font-size: 11px;
  text-align: left;
}

.rptPositioner {
  width: 33%;
  display: block;
  transform-origin: 0px 0px;
  transform: scale(3, 3);
}

.rptHeader {
  position: sticky !important;
  top: 0 !important;
  z-index: 1;
  background: #eee;
}

body {
  margin: 0;
  padding: 0;
}
<div class="rptDisplay">
  <div class="rptPositioner">
    <div class="rptHeader">Header</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
  </div>
</div>

问题在于当我滚动时,页眉下滑的速度与页面本身下滑的速度不同,存在一些非常奇怪的行为。请记住,transform scale 可以被用户更改用于缩放,而 sticky position 是最近添加的。

我追踪到问题出现在 rptDisplay 上没有应用缩放,但应用在 display 的子元素上,是粘性元素的父级。如果我将缩放应用于 display 或 header 上,则问题会消失,但目前这不是一个选项。

我附上了一个 CodePen,展示了我们看到的问题。

https://codepen.io/steves165/pen/QWOLMwr


你不是第一个注意到这个问题的人:#205: Sticky Positioning: How it Works, What Can Break It, and Dumb Tricks | CSS-Tricks - "使用scale()rotate()等变换,position:sticky的行为可能会变得非常奇怪。" - Richard Deeming
在 @RichardDeeming 的回答基础上,还需要将 .sticky 中的 top 值设置为 header 的高度,并添加 z-index 属性。 - Tasos Tsournos
2
你正在为你的粘性标题创建一个新的坐标系:https://dev59.com/RGUp5IYBdhLWcg3wj4Lu#15256339 - morganney
5个回答

3

有一个解决方案是根据当前的变换比例值自己设置top

你提到了,“变换比例是用户可更改的用于缩放”,这意味着正在使用JavaScript。因此,我们可以添加以下简短的代码来自己设置top

let scale = zoom.value;
let disp = document.querySelector('.rptDisplay');
let psnr = document.querySelector('.rptPositioner');
let header = document.querySelector('.rptHeader');

disp.addEventListener('scroll', setTop);

function setTop() {
  let offset = disp.scrollTop - (disp.scrollTop / scale);
  header.style.top = `${-offset}px`;
}

// for the demo set custom zoom
function adjust(r) {
  scale = r.value;
  psnr.style.transform = `scale(${scale},${scale})`;
  setTop()
}
.rptDisplay {
  overflow: auto;
  position: relative;
  top: 0;
  left: 0;
  z-index: 0;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 155px;
  max-height: 100vh;
  font-size: 11px;
  text-align: left;
}

.rptPositioner {
  width: 33%;
  display: block;
  transform-origin: 0px 0px;
  transform: scale(3, 3);
}

.rptHeader {
  position: sticky !important;
  /* top: 0 !important;*/
  z-index: 1;
  background: rgb(215, 253, 199);
}

body {
  margin: 0;
  padding: 0;
}
<label>Set Zoom(1-4):
    <input type="range" id="zoom" min="1" max="4" value="3" onchange="adjust(this)">
  </label><br>
<hr>
<div class="rptDisplay">
  <div class="rptPositioner">
    <div class="rptHeader">Header</div>
    <div class="row">Row first</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row Last</div>
  </div>
</div>


2
这个例子非常简单且通用,但它有一个副作用。由于不断的重新定位,滚动时会出现滞后抖动效果。在移动设备上尤其明显,快速平滑滚动时更为明显。 不幸的是,只有避免滚动时重新计算才能避免这个问题,就像这个例子中所做的那样。 - Oleg Barabanov
这是因为滚动事件并不会在每个像素变化时触发。当您快速滚动时,浏览器会限制事件的频率,即它不会经常触发。 - the Hutt

1
我将添加一个解决此问题的示例。此示例使用以下自定义CSS变量(您可以设置自己的):
  • --scale - 设置列表的缩放级别(而不是transform:scale())。通过更改此CSS变量(直接,在CSS规则中或通过JS),比例也会发生变化。
  • --width--height - 我们为列表中的每个元素使用JS设置的值,如果该项然后更改其大小,则使用 ResizeObserver变量将更新。

示例的逻辑基于我们不会增加整个列表,而是单独增加每个元素。为此,对每个元素添加了等于“(比例-1)*高度”的margin-bottom,以便出现空白区域,该区域将用transform:scale()填充。类似的效果还有margin-right--width

下面是示例:

// tracking changes in the size of elements
const resizeObserver = new ResizeObserver((entries) => {
  entries.forEach(({ target }) => {
    target.style.setProperty("--height", target.offsetHeight);
    target.style.setProperty("--width", target.offsetWidth);
  });
});

// initialization of basic parameters and observers
document.querySelectorAll(".rptPositioner > div").forEach((el) => {
  el.style.setProperty("--height", el.offsetHeight);
  el.style.setProperty("--width", el.offsetWidth);
  resizeObserver.observe(el);
});

// control of the zoom control
document.querySelector("#scale-control").onchange = function () {
  document
    .querySelector(".rptDisplay")
    .style.setProperty("--scale", this.value);
};
.rptDisplay {
  overflow: auto;
  position: relative;
  top: 0;
  left: 0;
  z-index: 0;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 170px;
  max-height: 100vh;
  font-size: 11px;
  text-align: left;
}

.rptPositioner {
  width: calc(100% / var(--scale, 1));
}

.rptPositioner>div {
  transform-origin: 0px 0px;
  transform: scale(var(--scale, 1));
  margin-bottom: calc(1px * var(--height) * (var(--scale, 1) - 1));
  margin-right: calc(1px * var(--width) * (var(--scale, 1) - 1));
  min-width: 100%;
}

.rptHeader {
  position: sticky !important;
  top: 0px !important;
  z-index: 999;
  background: #eee;
}

body {
  margin: 0;
  padding: 0;
}
<label>
Scale: 
<input type="number" id="scale-control" min="0.5" step="0.5" value="3">
</label>

<div class="rptDisplay" style="--scale: 3;">
  <div class="rptPositioner">
    <div class="rptHeader">Header1 Header1 Header1 Header1 Header1 Header1</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="rptHeader">Header3 Header3 Header3 Header3 Header3 Header3</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
  </div>
</div>

为了清晰起见,如果您需要在X轴和Y轴上使用不同的比例尺,则可以创建两个单独的变量,例如--scale-x--scale-y并分别使用它们。
需要注意的是,列表元素本身没有缩放(在示例中是.rptPositioner),但首先--scale变量可用于该元素,因此您可以直接缩放CSS属性(例如:border-width: calc((var(--scale) - 1) * 1px)),其次,可以通过ResizeObserver监视列表元素以获取其参数(宽度、高度等)。

+1. 是的,CSS 中的计算比 JS 更现代化/更快。如果你只缩放列表项而不是整个列表,那么它们就会溢出。在你的解决方案中,如果你在 rptPositioner 上加上边框,你会注意到所有行都会溢出。 - the Hutt
@onkarruikar,感谢您的关注。没错,列表本身不会缩放,因此行元素会重叠在其上,但它们不会重叠在容器元素.rptDisplay上。 然而,一些列表参数可以手动缩放,因为列表也有--scale变量可用。我在回应中添加了这个澄清。我的示例非常简单,具体任务需要根据情况进行扩展。 - Oleg Barabanov
是的,您需要根据 --scale 调整列表的 margin-right 或 width。 - the Hutt
@onkarruikar,我稍微更新了一下示例,考虑到您的建议,并像您的示例一样添加了一个缩放控制表单以提高清晰度。在示例中,可能会出现错误,例如“ResizeObserver loop limit exceeded”- 这里是为什么可以忽略此错误的解释。对于最终版本,根据情况可能仍需要详细的CSS更正,但我希望这个示例可以作为某人的基础。 - Oleg Barabanov

0

.rptDisplay {
  overflow: auto;
  position: relative;
  top: 0;
  left: 0;
  z-index: 0;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 200px;
  max-height: 100vh;
  font-size: 11px;
  text-align: left;
}

.rptPositioner {
  width: 33%;
  display: block;
  transform-origin: 0px 0px;
  transform: scale(3, 3);
}

.rptHeader {
  position: sticky !important;
  top: 0 !important;
  z-index: 1;
  background: #eee;
  width: 33%;
  transform-origin: 0px 0px;
  transform: scale(3, 3);/*same style or zoom*/
}

body {
  margin: 0;
  padding: 0;
}
<div class="rptDisplay">
  <div class="rptHeader">Header</div><!-- took out of positioner -->
  <div class="rptPositioner">
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
  </div>
</div>

想法

  • 简单的方法是将rptHeaderrptPositioner中取出,并将其作为同级而不是子级放置
  • 既然我们已经取出了,现在我们将相同的缩放应用于rptHeader

P.S

看起来非常复杂的问题可能有像这样用简单的HTML解决的方法。

-1
如果你对应用比例和粘性标题感兴趣,你可以直接将其应用于rptDisplay,如下所示。

.rptDisplay {
  height: 120px;
  width: fit-content;
  margin: 10px;
  padding-right: 50px;
  overflow: auto;
  position: relative;
  transform-origin: 0 0;
  transform: scale(1.5);
}

.rptHeader {
  position: sticky;
  top: 0;
  background: white;
}
<div class="rptDisplay">
  <div class="rptPositioner">
    <div class="rptHeader">header 1</div>
    <div class="row">Row First - Header 1</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row Last - Header 1</div>
    <div class="rptHeader">header 2</div>
    <div class="row">Row First - Header 2</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row</div>
    <div class="row">Row Last - Header 2</div>
  </div>
</div>


这不好,如果你拖动滚动条根本不起作用。 - DATEx2
您介意分享一下您使用的浏览器和操作系统吗?我在 Windows 操作系统中测试了 Firefox、Chrome 和 Edge,都正常工作。 - Gangula
之前的代码片段导致了两个滚动条,因为在Stack Overflow中的结果窗口太小了。所以我修改了代码片段以适应结果窗口。现在可以清楚地进行交互并查看结果了。 - Gangula

-1

我会创建一个新的

transform-origin 可能会增加滚动速率到 x3
所以尝试使用 Zoom, caniuse zoom

它是非标准的,最初只在Internet Explorer中实现。虽然现在有几个其他浏览器支持缩放,但不建议在生产网站中使用。

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.rptDisplay {
  padding-top: 50px;/* <--- to show the stick effect*/
  position: relative;
  zoom: 3; /*but this has less browser suppoert*/
}

.rptHeader {
  position: sticky;
  top: 0;
  background: #eee;
}
<div class="rptDisplay">
  <div class="rptHeader">Header</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
  <div class="row">Row</div>
</div>


尝试在JS中增加高度,也许会起作用。 - Sarout

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接