20

I have a somewhat strange behaviour in Chrome and Safari. I have a scaled (transform: scale()) container with a video and other elements inside of it. At some scalings the absolute positioned elements with a high z-index disappears and does not come back again.

How can I fix this? Note that I cannot give the video element a negative z-index and I need to use overflow: hidden;.

Example

I have made an example that scales the outermost container up and down. At a specifik scale value the element with class .on-top (and text "I should always be on top.") disappears. When scaling down again it suddenly appears.

Link to exmaple: https://jsfiddle.net/iafiawik/Lcox1ecc/

enter image description here

Conclusions

  • It seems like the size of the element matters. The larger I make it, the larger is the scale value before it disappears.
  • I have also tested to set transform: scale(1.4) with CSS directly on the element and the behaviour is the same.

The issue does not exist if I:

  • Replace the video tag with a div
  • Remove position: absolute; from siblings to .on-top (that is, .below)
  • Remove overflow: hidden; from .content
  • If I move .on-top so it is placed after the video tag in the document flow

(But of course none of these workarounds work for me in reality because of project specific reasons. I also cannot give the video element a negative z-index and I need to use overflow: hidden;.)

Suggested workarounds from the community (thanks!)

  • Give the video tag a negative z-index (can't do this because I sometimes have elements placed behind the video)
  • Remove overflow: hidden; (I can't remove overflow: hidden;)

Browsers

I have seen this issue in Chrome (Mac) and Safari (Mac).


Update 1

Seems like this bug report pretty much covers my problem. However, it does not provide a fix for it.

Update 2

I've answered my own question by providing my solution to this problem.

Update 3

There are a lot of answers coming in that either modify the z-index of the video or adds translateZ to the .on-top element. Demos have shown that both of those approaches do fix the issue. However, since my HTML structure is the output from a visual HTML editor (long story ...), I do not know what elements will be there or if they should be in front, below or next to a video. Therefore I am looking for a solution that does not require changes to individual elements that are inside the scaled element.

iafiawik
  • 433
  • 3
  • 13
  • 3
    Apparently `scale` messes up the z-index info in Safari and Chrome. Check this: https://stackoverflow.com/questions/35681829/transformscale-breaking-my-z-index-order?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa There are some workarounds, but it seems you have figured them out already! – José A. Zapata Apr 04 '18 at 16:43
  • 1
    Thanks José! I have tried the workarounds but they did not help me. I have to use `overflow: hidden;` at some point and this seems to be the problem for me (when combined with `position: relative;` or `position: absolute;`. – iafiawik Apr 05 '18 at 14:02
  • 1
    It looks like this is a webkit/blink (safari/chrome/opera) issue! I did not encounter this problem on Firefox, Edge and IE11 – Baksteen Apr 12 '18 at 11:46
  • @Baksteen Same for me. I've only been able to reproduce it in Chrome and Safari. – iafiawik Apr 12 '18 at 11:55

9 Answers9

9

It looks like a bug in Chrome. Notice that when you scale the image, the element inspector keeps telling you that the size of #scaled is 1024x768:

Chrome Scale

Where as in Firefox:

Firefox Scale

Now, apparently, Chrome uses the wrong size to conclude that .on-top is completely outside .content and hides it because of hidden overflow (it should not be doing this but apparently it is trying to optimize away any element that displays above a video). Examples:

Scale: 1.225
Parent width: 1254.40
Child left: 1254.40 - (100 + 90) * 1.225 = 1021.65
Result: less than 1024 (partially inside)

Scale: 1.230
Parent width: 1259.52
Child left: 1259.52 - (100 + 90) * 1.230 = 1025.82
Result: greater than 1024 (completely outside)

Unfortunately I could not find an elegant solution. Ideally you should revise your HTML markup and CSS, perhaps align the top element with left edge. As a last resort, you can move the elements more towards left using transparent border:

var goalScale = 140;
var startScale = 100;
var currentScale = 100;
var shouldScaleUp = true;
var container = document.getElementById("scaled");
var scaleInfo = document.getElementById("scale-info");

function step() {
  container.style.transform = "scale(" + (currentScale / 100) + ")";
  scaleInfo.innerText = "Scale: " + (currentScale / 100);
  if (currentScale === goalScale) {
    shouldScaleUp = false;
  }
  if (currentScale === startScale) {
    shouldScaleUp = true;
  }
  if (shouldScaleUp) {
    currentScale += 0.5;
  } else {
    currentScale -= 0.5;
  }
  window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
.scale-info {
  position: fixed;
  left: 0;
  top: 0;
}

#scaled {
  background: #cccccc;
  width: 1024px;
  height: 768px;
  position: fixed;
  left: 200px;
  top: 200px;
}

.content {
  height: 100%;
  overflow: hidden;
  position: relative;
  margin-left: auto;
  margin-right: auto;
  background: rgba(34, 34, 56, 0.2);
}

.below {
  position: absolute;
  width: 300px;
  height: 300px;
  right: 0px;
  top: 100px;
  background: purple;
  z-index: 1;
  opacity: 0.8;
}

.below-2 {
  z-index: 3;
  right: 100px;
}

.below-3 {
  z-index: 4;
  right: 400px;
}

.on-top {
  position: absolute;
  width: 50px;
  right: 100px;
  top: 150px;
  background: pink;
  z-index: 5;
  padding: 20px;
  /* a 200px border moves the element towards left */
  border-left: 200px solid transparent;
  background-clip: padding-box;
}

.on-top h1 {
  font-size: 20px;
}

#video {
  position: absolute;
  z-index: 4;
  width: 1024px;
  height: 768px;
  background: rgba(0, 0, 0, 0.4);
}
<div id="scale-info"></div>

<div id="scaled">
  <div class="content">
    <h2 class="below below-1"> I have z-index 1</h2>
    <div class="on-top">
      <h1> I should always be on top.<br> I have z-index 5</h1>
    </div>
    <h2 class="below below-2"> I have z-index 3</h2> <video id="video" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
    <h2 class="below below-3"> I have z-index 4</h2>
  </div>
</div>
Salman A
  • 262,204
  • 82
  • 430
  • 521
  • Thanks for pointing this out :) I created an updated demo with the new insights: https://jsfiddle.net/iafiawik/bpos8nw3/ I added an outer element which is not scaled but gets updated width and height values as the inner element is scaled up and down. I will give this a try and see how it works in a real project. – iafiawik Apr 12 '18 at 12:44
  • 2
    @iafiawik see revised answer. – Salman A Apr 12 '18 at 13:06
  • Thanks for you detailed answer, really appreciate it. I would like to avoid having to manipulate individual elements to prevent them from disappearing. As far as I can tell, the thing that makes your solution works is the fact that you add a 200px `border-left` to `.on-top`. This is probably a good enough solution if you have a limited amount of elements above the video, but that's the not the case for me. My HTML is the output from a visual editor, so I won't know what elements will be there when the element is to be rendered. – iafiawik Apr 12 '18 at 13:35
  • Hi again, I've used your answer in my solution (it is posted as an answer). Thanks for the help :) – iafiawik Apr 16 '18 at 14:31
8

Here you go: https://jsfiddle.net/Lcox1ecc/423/

You just need to add -webkit-transform: translateZ(0); to the .on-top class.

Happy Coding!

jmcgriz
  • 2,819
  • 1
  • 9
  • 11
  • This is definitely an option for those with a few/controlled number of elements. Sadly for me, my HTML is the output from a visual editor, so I won't know what elements will be there when the document is to be rendered. I could of course add `translateZ` to all elements (via some class), but I find that to be more work than my current workaround. Thanks, anyway :)! – iafiawik Apr 18 '18 at 10:36
  • 1
    Ah ok, didn't realize that was your use case. For a global fix, just use `* {  -webkit-transform: translateZ(0); }`. That'll apply the rule to everything. It shouldn't have any effects on your layout other than correcting that display bug. – jmcgriz Apr 18 '18 at 11:16
  • Is that a heavy operation? – iafiawik Apr 18 '18 at 11:50
  • 1
    If you know what type of elements will be in that position, it'd always be better to list them specifically, i.e. `h1, h2, div { }`, but when used sparingly and in the right circumstances, `*` is fine to use and won't massacre your load time or anything. – jmcgriz Apr 18 '18 at 11:56
  • Thanks, I'll give it a go :) I've also updated my question a bit regarding my HTML structure. – iafiawik Apr 18 '18 at 12:47
  • Turns out that some elements have their own `transform` styling (not necessarily `translateZ`), and this is not merged but overridden by the `*` selector. – iafiawik Apr 18 '18 at 13:00
  • It shouldn't as long as you put that line before all of your other styles. It has the least priority of any selector there is so it should get overridden by everything that comes after it. Meant to say to place it at the top of your stylesheet in my previous comment. – jmcgriz Apr 18 '18 at 13:53
  • Yes, but does it solve the problem if some elements that have their own `transform` styling don't get the `translateZ` fix? Let's say that I have six elements on top of the video and five of them have `transform: scaleX(0.6);` and therefore don't get the `translateZ` fix, all I have left is one element with `translateZ`. – iafiawik Apr 18 '18 at 15:22
4

After spending a lot of time researching this problem and trying a lot of different approaches I've come to the conclusion that no solution fixes my problem. There are solutions that fix the problem if you are able to control the z-indexes of the elements that disappear, but I am unable to do so since the structure of the HTML is not known to be (it is the output of the HTML editor). I was looking for a solution that would not require changes to individual children to the scaled parent, but I have not found any so far.

This bug report pretty much covers my problem but it does not provide a fix for it.

I can confirm that this happens because the element is outside of the scaled containers original width and height:

The element is visible at scale(1.227) (red border indicates the original size of #scaled):

enter image description here

... but not at scale(1.228):

enter image description here

My solution is therefore to add another wrapping element outside the scaled element that is not scaled, but get its width and height properties updated according to its first child scale values. This element has overflow: hidden; and prevents elements from being visible.

enter image description here

This is not a perfect solution as one might experience a small gap between the scaled element and the outermost wrapping element (rounding issues), but it is the best I can do given the circumstances.

var goalScale = 140;
var startScale = 100;
var currentScale = 100;
var shouldScaleUp = true;
var container = document.getElementById("scaled");
var scaledContainer = document.getElementById("resized-container");
var scaleInfo = document.getElementById("scale-info");

function step() {
  var contentWidth = 1024;
  var contentHeight = 768;
  container.style.transform = "scale(" + (currentScale / 100) + ")";

  scaledContainer.style.width = contentWidth * ((currentScale / 100)) + "px";
  scaledContainer.style.height = contentHeight * ((currentScale / 100)) + "px";

  scaleInfo.innerText = "Scale: " + (currentScale / 100);

  if (currentScale === goalScale) {
    shouldScaleUp = false;
  }
  if (currentScale === startScale) {
    shouldScaleUp = true;
  }

  if (shouldScaleUp) {
    currentScale += 0.5;
  } else {
    currentScale -= 0.5;
  }

  window.requestAnimationFrame(step);
}

window.requestAnimationFrame(step);
#resized-container {
  position: fixed;
  width: 1024px;
  height: 768px;
  overflow: hidden;
  border: 10px solid red;
  top: 200px;
  left: 200px;
}

#scaled {
  background: #cccccc;
  width: 1024px;
  height: 768px;
  position: absolute;
  transform-origin: left top;
}

.content {
  height: 100%;
  position: relative;
  margin-left: auto;
  margin-right: auto;
  background: rgba(34, 34, 56, 0.2);
}

.below {
  position: absolute;
  width: 300px;
  height: 300px;
  right: 0px;
  top: 100px;
  background: purple;
  z-index: 1;
  opacity: 0.8;
}

.below-2 {
  z-index: 3;
  right: 100px;
}

.below-3 {
  z-index: 4;
  right: 400px;
}

.on-top {
  position: absolute;
  width: 50px;
  right: -30px;
  top: 150px;
  background: pink;
  z-index: 5;
  padding: 20px;
}

.on-top h1 {
  font-size: 20px;
}

#video {
  position: absolute;
  z-index: 4;
  width: 1024px;
  height: 768px;
  background: rgba(0, 0, 0, 0.4);
}
<div id="resized-container">
  <div id="scaled">
    <div id="scale-info">

    </div>
    <div class="content">
      <h2 class="below below-1">
        I have z-index 1
      </h2>

      <div class="on-top">
        <h1>
          I should always be on top.<br /> I have z-index 5
        </h1>
      </div>

      <h2 class="below below-2">
        I have z-index 3
      </h2>
      <video id="video" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
      <h2 class="below below-3">
        I have z-index 4
      </h2>
    </div>
  </div>
</div>
iafiawik
  • 433
  • 3
  • 13
  • Check my answer below. It's an odd glitch with rendering in webkit that I've run into while developing browser based games over the years, that one line should fix it. Sorry to not catch the question before you put all that effort into a workaround – jmcgriz Apr 17 '18 at 18:56
2

One approach, if you can modify a bit your html, is wrap your problematic elements in a container that is the same size as the video and container, with the proper z-index. That way you would have clear layers of the same size and positions, into which you can position more complex elements. Like this for example:

   <div id="top-container">
     <div class="on-top">
      <h1>
        I should always be on top.<br /> I have z-index 5
      </h1>
    </div>
   </div>

  #top-container {
    width: 100%;
    height: 100%;
    position: absolute;
    z-index: 5;
  }

https://jsfiddle.net/06oykj8o/4/

Julien Grégoire
  • 16,864
  • 4
  • 32
  • 57
  • Hi, thanks for your answer! This might actually be a workaround for those with a simple HTML structure. :) Unfortunately for me, my HTML is the output from a visual editor, so I won't know what elements will be there when the page is to be rendered. This also means that I cannot set each individual element to `width: 100%; height: 100%;` since this would mess up pointer-events and such things. – iafiawik Apr 13 '18 at 06:57
1

I made this workaround by puttingz-index:-1; on video.

https://jsfiddle.net/Lcox1ecc/312/

Sandwell
  • 1,451
  • 2
  • 16
  • 31
  • Hi! Thanks for your input. I don't mean to sound ungrateful but the question already mentions that removing `overflow: hidden` makes the issue go away. So `overflow: hidden` is clearly there for a reason. Regarding the other suggestion, if you could provide a working example with a "node between" that functions the same as `overflow: hidden`, removes the buggy behavior but keeps everything else the same, then that would be great. – jonahe Apr 12 '18 at 09:21
  • 1
    Arf yes sorry. I missed this. I rushed into the jsfiddle – Sandwell Apr 12 '18 at 09:37
  • @Sandwell No problems, thanks! Do you have any working example with the extra node? – iafiawik Apr 12 '18 at 09:40
  • @iafiawik I tried but it did not work well. Could you consider using a negative z-index on the video tag like my updated jsfiddle ? – Sandwell Apr 12 '18 at 09:52
  • @Sandwell I realise this is one of few workarounds that seems fairly simple, but ... I have other elements that are placed behind the video at some times and putting a negative z-index on the video gives me a bunch of new problems. – iafiawik Apr 12 '18 at 11:10
  • @iafiawik you can also use negatives z-index on theses elements. – Sandwell Apr 12 '18 at 12:04
  • @Sandwell I could ... but it would be a lot of work since the HTML is not known/controlled by me - it is the output from an HTML editor. Negative z-indexes could also affect pointer-events and transparency animations. – iafiawik Apr 16 '18 at 06:55
1

I really like the answer from Salman A. The only thing that comes to mind, would be rewriting with position: relative. But I don't know if that is an option.

Domenik Reitzner
  • 1,583
  • 1
  • 12
  • 21
  • 1
    I agree, it is the best answer yet. Unfortunately, rewriting to `position: relative;` is not an option at this point - it'd take too much time. – iafiawik Apr 12 '18 at 13:25
0

I stumbled across something similar to this last week with positioning absolute elements and transforms...

I dunno if this will help you out but here is a link.

CSS transform: translate moves postion:fixed inner Div

In the end I fixed it by using a transform: translateX(none) vs translateX(0).

Super strange behavior for sure, but the link gives some more links to help make things more clear - as in its behaving per spec.

Sten Muchow
  • 6,623
  • 4
  • 36
  • 46
  • Thanks for the response! Have you seen if you could get this trick to work with the JSFiddle demo (minimal example reproducing the behavior) linked in the question? It's not obvious to me how this approach would be applied to the specifics of the demo. – jonahe Apr 11 '18 at 20:19
  • 1
    Hello! Thanks for your input. I do not know how to apply this to my example (https://jsfiddle.net/iafiawik/Lcox1ecc/). My problem is z-index, not positioning, but maybe they're somehow related? If so, how? – iafiawik Apr 12 '18 at 06:59
0

It is happening because of overflow is hidden.Here is working link

https://jsfiddle.net/Lcox1ecc/322/

.content {
overflow:visible;
}
Pramod
  • 666
  • 3
  • 11
  • 1
    Yes, I know (this info was included in my question). I need to make it work with `overflow: hidden;`. Do you have any ideas how to make it work? – iafiawik Apr 12 '18 at 10:52
0

It might be late but just posting in case somebody finds it helpful. Add an empty div under the parent container element with transform animation and nothing will disappear anymore. The animation does not do anything but it forces the browser to render all the elements using hardware acceleration.

<div class="emptydiv"></div>

    .emptydiv{
      transform:scale(1);
      animation:fix 3s infinite;
    }
    
    @keyframes fix{
      50%{
        transform:scale(1);
      }
    }