Search code examples
jquerygoogle-chromescrollbarchromiumopacity

Scrollbar is disappearing/reappearing when element changes opacity in Chrome


I have a div called img-with-overlay that contains a static-positioned image and an absolute-positioned overlay. When img-with-overlay is hovered over, the overlay goes from 0 opacity to 1. An unintended side-effect of this is that the vertical scrollbar disappears when the overlay is opaque, in Chrome and Chromium-based browsers. It does not occur in Firefox.

Why is the scrollbar there, you ask? You can see that there is a container with a very specific width and height. If these dimensions are altered by even half a pixel, the unintended side-effect will not occur. I'm using this container to represent the body of my webpage, although I have shrunk it for the purposes of fitting into a snippet. While most screen sizes will not present an issue, I still want to prevent it from over occurring.

$(document).ready(function () {
  $('.img-with-overlay').hover(function () {
    $('.overlay').css('opacity',1);
  }, function () {
    $('.overlay').css('opacity',0);
  })
});
.container {
  width: 600px;
  height: 248.5px;
  overflow: auto;
  overflow-x: hidden;
}

.img-with-overlay {
  position: relative;
  overflow: hidden;
  width: 100%;
}

img {
  display: block;
  width: 100%;
}

.overlay {
  position: absolute;
  top: 0;
  opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
  <div class="img-with-overlay">
    <img src="https://via.placeholder.com/468x200?text=Hover">
    <div class="overlay">This is an overlay</div>
  </div>
</div>

Why is the scrollbar disappearing/reappearing when the opacity of the overlay changes?

Some interesting things I've noted:

  • If the img is replaced with a div of the same dimensions, the scrollbar does not appear.
  • If the overlay opacity is set to a value less than 1, such as 0.7, the scrollbar will not disappear on hover.

Solution

  • What is happening is very simple, but not so obvious.

    Background

    When you set the image width: 100%, the image height is computed automatically to preserve the original proportions. The container is tall just a bit less than the image, and since it has overflow-y: auto, the vertical scroll appears. But now the magic happens: the vertical scrollbar reduces the container inner width, so the image width reduces accordingly and its height do so to preserve proportions. But now the image is tall just a bit less than the container, so there is no more need of the vertical scrollbar. But if the scrollbar would disappear, the image would expand again starting an infinite loop with everything flickering. To prevent this race condition, the browser choose to always show the scrollbar, but since the image fit in the container there is no need to scroll down, so the scrollbar gets disabled.

    This explains why changing a bit the sizes solves the problem: if you make the container a bit narrow or a bit taller the image always fit without the need of the scrollbar; if you make the container a bit wider or a bit shorter the image never fit and scrollbar is always shown.

    The opacity problem

    Now consider the following. Your image has both display: block and width: 100%. Both attributes affect the same property (the width) but in different ways and at different time in the layout workflow. This is just enough to start a race condition, which behaviour is not defined and the browser tries to solve it its own way. You can verify it by opening the inspector and toggling the display attribute of the img: at the beginning you have a disabled scrollbar, then when you turn off the attribute the scrollbar gets enabled, and then when you renable the attribute the scrollbar completely disappears. What?! We are in the initial condition again, but the result is not the same?! :OMG: Most of the times this race condition has no effect, but in this case it became clear because of what described in the previous section.

    Now let's talk about the opacity. Elements with opacity less than 1 must be rendered in a specific order (back to front) and after back opaque elements are rendered. Although I don't know how this is exactly implemented in Chrome vs Firefox, the behaviour suggests that in Chrome the rendering order affects someway the layout workflow. This explains why opacity=1 behave different than opacity=0.999. But this only happens because there is a condition which has no defined behaviour, as explained before.

    So the solutions is very simple: remove the condition which causes the undefined behaviour. Since display: block doesn't affect the image size, but only the space the element occupies, the thing to do is obvious: delete the display: block and leave only width: 100% on the img element.

    If for some reason you don't want to remove the display: block, you may force the scrollbar to be always visible by coding overflow-y: scroll on the container (and also remove the overflow attribute). This way you don't remove the condition which causes the undefined behaviour, but you remove the race condition on the scrollbar.