'CSS Position Sticky weird behvaiour when scale applied to parent, sticky scrolls and different speed to element

I can't believe that no one has noticed this before but I can't seem to find anyone that has noticed this behaviour. I should start off by saying that the html for this was written a long time ago (not by me) and can't really be modified at the moment.

So here is the problem:

We have html structured laid out like this

.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>

The trouble is there is some very weird behaviour, when I scroll the header scrolls down the page at a different rate to the page scrolling down itself. Bare in mind that the transform scale is user changable for zooming, the sticky position was added recently.

I tracked the issue down to the scaling not being applied to the rptDisplay but being applied to a child of display but a parent of the sticky element. If I apply the scaling to the display or to the header the problem goes away, but that isnt currently an option.

I have attached a CodePen that demostrates the issue we are seeing.

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



Solution 1:[1]

One solution could be to set top ourselves based on the current transform scale value.

You've mentioned that, "the transform scale is user changable for zooming." That means javascript is being used. So we can add following small code to set the top ourselves:

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>

Solution 2:[2]

I will add an example of solving this problem. This example uses the following custom CSS Variables (you can set your own):

  • --scale - sets the scaling level of the list (instead of transform: scale()). By changing this CSS variable (directly, in CSS rules or via JS), the scale will also change.
  • --width and --height - values, which we set using JS for each element of the list,and if the item then change its size, then using ResizeObserver variables will be updated.

The logic of the example is based on the fact that we do not increase the entire list, but increases each element individually. To do this, a margin-bottom equal to "(scale - 1) * height" is added to each element, so that an empty space appears, which will be filled with transform: scale(). Similar effect with margin-right and --width.

Example below:

// 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>

Example for clarity, simplified if you need different scales on the X-axis and on the Y-axis, then you can create two separate variables, for example, --scale-x and --scale-y and use them separately.

It is important to note that the list element itself is not scaled (in the example it is .rptPositioner), but firstly the --scale variable is available to this element and for this reason you can do direct scaling of CSS properties (for example: border-width: calc((var(--scale) - 1) * 1px), and secondly - this the list element can also be monitored through ResizeObserver to get this parameters (width, height, etc.).

Solution 3:[3]

.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>

Idea

  • Simple take the rptHeader out of rptPositioner and place it as sibling instead of child
  • since we have took out now we apply the same zoom to rptHeader

P.S

Problems that look highly complicated might have solutions like this with simple html

Solution 4:[4]

If applying scale along with sticky header is what you're interested in, you can apply it directly to rptDisplay like below

.rptDisplay {
  height: 400px;
  width: 300px;
  overflow: auto;
  position: relative;
  transform-origin: 0 0;
  transform: scale(2);
}

.rptHeader {
  position: sticky;
  top: 0;
  background: white;
}
<div class="rptDisplay">
  <div class="rptPositioner">
    <div class="rptHeader">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</div>
    <div class="row">Row</div>
    <div class="rptHeader">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</div>
    <div class="row">Row</div>
  </div>
</div>

Solution 5:[5]

I would create a new one

the transform-origin may increase the scroll rate to x3
So try Zoom, caniuse zoom

It is non-standard, and was originally implemented only in Internet Explorer. Although several other browsers now support zoom, it isn't recommended for production sites.

* {
  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>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 the Hutt
Solution 2
Solution 3
Solution 4 Gangula
Solution 5