6

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

Richard Deeming
  • 29,830
  • 10
  • 79
  • 151
steves165
  • 74
  • 2
  • 7
  • You're not the first to notice it: [#205: Sticky Positioning: How it Works, What Can Break It, and Dumb Tricks | CSS-Tricks](https://css-tricks.com/video-screencasts/205-sticky-positioning-how-it-works-what-can-break-it-and-dumb-tricks/#aa-weird-tricks) - *"Usings transforms like `scale()` and `rotate()`, the behavior of `position:sticky` can get pretty weird"* – Richard Deeming Jan 26 '22 at 14:16
  • To add on @RichardDeeming answer, also make top in .sticky same as the height of the header. Also add a z-index. – Tasos Tsournos Jan 26 '22 at 14:18
  • 2
    You are creating a new coordinate system for your sticky header: https://stackoverflow.com/a/15256339/258174 – morganney Jan 26 '22 at 14:18

5 Answers5

3

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>
the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • 2
    This example is very simple and universal, but it has one side effect. Due to the constant repositioning, there is a lagging jitter effect when scrolling. This is especially noticeable on mobile devices, with fast and smooth scrolling. Unfortunately, this problem can be avoided only if you avoid recalculation when scrolling, like it is done [in this example](https://stackoverflow.com/a/70934546). – Oleg Barabanov Feb 01 '22 at 01:47
  • It's happening because the scroll event isn't fired for each pixel change. When you scroll fast browser throttles the event i.e it isn't fired frequently. – the Hutt Feb 01 '22 at 04:09
1

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

Oleg Barabanov
  • 2,468
  • 2
  • 8
  • 17
  • +1. Yes, calculations in CSS are more uptodate/faster than JS. If you scale only list items and not the list then they will overflow. In your solution, if you put border on rptPositioner you'll notice that all rows are overflowing. – the Hutt Feb 01 '22 at 03:56
  • @onkarruikar, Thank you for your attention. That's right, the list itself does not scale, and therefore the row elements overlap it, but they do not overlap the container element `.rptDisplay`. However, some of the list parameters can be manually scaled, because the `--scale` variable is also available for the list. I added this clarification in response. My example is very simple and depending on the specific task, it will need to be extended. – Oleg Barabanov Feb 01 '22 at 07:49
  • Yes you need to adjust margin-right or width of the list as per `--scale`. – the Hutt Feb 01 '22 at 08:48
  • @onkarruikar, I have updated the example a little, taking into account your recommendations, and as in your example, I have added a scaling control form for clarity. In the example, errors may appear, such as "ResizeObserver loop limit exceeded" - [here is an explanation of why this error can be ignored](https://stackoverflow.com/a/50387233/17182878). For the final version, detailed CSS corrections may still be required, depending on the case, but I hope this example can help someone as a basis. – Oleg Barabanov Feb 01 '22 at 14:25
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>

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

Neptotech -vishnu
  • 1,096
  • 8
  • 23
-1

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>
Sarout
  • 821
  • 4
  • 25
-1

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

.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>
Gangula
  • 5,193
  • 4
  • 30
  • 59
  • This is not good, if you drag the scroll bar doesn't work at all – DATEx2 Jun 25 '23 at 22:56
  • would you mind sharing which browser and OS you're using? I tested in Firefox, chrome and edge in Windows OS and its working fine – Gangula Jun 26 '23 at 06:09
  • Also the earlier code snippet resulted in 2 scroll-bars since the result window in stack overflow is too small. So I modified the code snippet to fit in the result window. Its clear to interact and see the result now. – Gangula Jun 26 '23 at 06:11