Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue while using transitions + opacity change + overflow hidden

If you see the code sample I have shared, you can see the overlay going outside the box. I traced the issue down to the transition attribute.

I want to remove the content outside of the div. Overflow isn't working as it is supposed to. (removing transition works, but I would like to keep it if possible)

Any help is appreciated

Codepen Link

CODE

var timer = setInterval(function() {
  document.querySelector(".qs-timer-overlay").style.opacity = (document.querySelector(".qs-timer-overlay").style.opacity * 1) + 0.1;
  if (document.querySelector(".qs-timer-overlay").style.opacity * 1 == 1) {
    clearInterval(timer);
  }
}, 1000);
.qs-main-header .qs-timer {
  padding: 13px 10px;
  min-width: 130px;
  text-align: center;
  display: inline-block;
  background-color: #dd8b3a;
  color: #FFF;
  font-size: 20px;
  border-radius: 50px;
  text-transform: uppercase;
  float: right;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.qs-main-header .qs-timer-overlay {
  z-index: 1;
  width: 10%;
  max-width: 100%;
  position: absolute;
  height: 100%;
  top: 0;
  left: 0;
  background-color: #c7543e;
  opacity: 0.0;
  /* border-radius: 50px 50px 0px 50px; */
}
.qs-main-header .qs-timer-content {
  z-index: 2;
  position: relative;
}
.scale-transition {
  -webkit-transition: all 1s;
  transition: all 1s;
}
<div class="qs-main-header">
  <div class="qs-timer scale-transition ng-hide" ng-show="visibility.timer">
    <div class="scale-transition qs-timer-overlay"></div>
    <div class="qs-timer-content ng-binding">0 <span class="ng-binding">Sec(s)</span>
    </div>
  </div>
</div>
like image 860
user1496463 Avatar asked Jul 29 '15 06:07

user1496463


2 Answers

Actually it is the border-radius that is not getting respected when the transition is happening. This is because of creation of compositing layers for accelerated rendering and can be explained by having a look at the following articles:

  • HTML5 Rocks - Accelerated Rendering in Chrome
  • GPU Accelerated Compositing in Chrome.

Why does the issue not happen when transition is disabled?

  • When styles change but none of the criteria that necessitates the creation of a compositing layer is satisfied (that is, no animation or transition or 3D transform etc):
    • There is no compositing layer and so the whole area seems to get repainted at every change. Since a full repaint happens there is no issue.
  • View the below snippet (in full screen mode) after enabling "Show paint rects" and "Show composited layer borders" from Dev tools and observe the following:
    • No areas with an orange border (compositing layer) are created.
    • Every time the styles are modified by setting the focus on one of the a tags, the whole area gets repainted (a red or green blinking area).

.outer {
  position: relative;
  height: 100px;
  width: 100px;
  margin-top: 50px;
  border: 1px solid red;
  overflow: hidden;
}
.border-radius {
  border-radius: 50px;
}
.inner {
  width: 50px;
  height: 50px;
  background-color: gray;
  opacity: 0.75;
}
a:focus + .outer.border-radius > .inner {
  transform: translateX(50px);
  height: 51px;
  opacity: 0.5;
}
<a href='#'>Test</a>
<div class='outer border-radius'>
  <div class='inner'>I am a strange root.
  </div>
</div>

Why does adding a transition create a problem?

  • Initial rendering has no compositing layer because there is no transition yet on the element. View the below snippet and note how when the snippet is run a paint (red or green blinking area) happens but no compositing layer (area with orange border) is created.
  • When transition starts, Chrome splits them into different compositing layers when some properties like opacity, transform etc are being transitioned. Notice how two areas with orange borders are displayed as soon as the focus is set on one of the anchor tags. These are the compositing layers that got created.
  • The layer splitting is happening for accelerated rendering. As mentioned in the HTML5 Rocks article, the opacity and transform changes are applied by changing the attributes of the compositing layer and no repainting occurs.
  • At the end of the transition, a repaint happens to merge all the layers back into a single layer because compositing layers are no longer applicable (based on criteria for creation of layers).

.outer {
  position: relative;
  height: 100px;
  width: 100px;
  margin-top: 50px;
  border: 1px solid red;
  overflow: hidden;
}
.border-radius {
  border-radius: 50px;
}
.inner {
  width: 50px;
  height: 50px;
  background-color: gray;
  transition: all 1s 5s;
  /*transition: height 1s 5s; /* uncomment this to see how other properties don't create a compositing layer */
  opacity: 0.75;
}
a:focus + .outer.border-radius > .inner {
  transform: translateX(50px);
  opacity: 0.5;
  /*height: 60px; */
}
<a href='#'>Test</a>
<div class='outer border-radius'>
  <div class='inner'>I am a strange root.
  </div>
</div>

This illustrates that when the layers are merged back and full repaint happens, the border-radius on the parent also gets applied and respected. However, during transition only the compositing layer's properties are changed, so the layer seems to become unaware of the properties of other layers and thus doesn't respect the border-radius of the parent.

I would assume this to be because of the way rendering of layers work. Each layer is a software bitmap and so it kind of becomes equivalent to having a circular image and then placing a div on top of it. That would obviously not result in any clipping of content.

The comment in this bug thread also seems to confirm that a repaint happens when a separate layer is no longer required.

We want to repaint if "gets own layer" is going to change

Note: Though they are Chrome specific, I think the behavior should be similar in others also.


What is the solution?

The solution seems to be to create a separate stacking context for the parent (.qs-timer) element. Creating a separate stacking context seems to result in a separate compositing layer being created for the parent and this solves the issue.

As mentioned by BoltClock in this answer, any one of the following options would create a separate stacking context for the parent and doing one of them seems to resolve the issue.

  • Setting a z-index on the parent .qs-timer to anything other than auto.

    var timer = setInterval(function() {
      document.querySelector(".qs-timer-overlay").style.opacity = (document.querySelector(".qs-timer-overlay").style.opacity * 1) + 0.1;
      if (document.querySelector(".qs-timer-overlay").style.opacity * 1 == 1) {
        clearInterval(timer);
      }
    }, 1000);
    .qs-main-header .qs-timer {
      padding: 13px 10px;
      min-width: 130px;
      text-align: center;
      display: inline-block;
      background-color: #dd8b3a;
      color: #FFF;
      font-size: 20px;
      border-radius: 50px;
      text-transform: uppercase;
      float: right;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      z-index: 1; /* creates a separate stacking context */
    }
    .qs-main-header .qs-timer-overlay {
      z-index: 1;
      width: 10%;
      max-width: 100%;
      position: absolute;
      height: 100%;
      top: 0;
      left: 0;
      background-color: #c7543e;
      opacity: 0.0;
      /* border-radius: 50px 50px 0px 50px; */
    }
    .qs-main-header .qs-timer-content {
      z-index: 2;
      position: relative;
    }
    .scale-transition {
      -webkit-transition: all 1s;
      transition: all 1s;
    }
    <div class="qs-main-header">
      <div class="qs-timer scale-transition ng-hide" ng-show="visibility.timer">
        <div class="scale-transition qs-timer-overlay"></div>
        <div class="qs-timer-content ng-binding">0 <span class="ng-binding">Sec(s)</span>
        </div>
      </div>
    </div>
  • Setting opacity to anything less than 1. I have used 0.99 in the below snippet as it doesn't cause any visual difference.

    var timer = setInterval(function() {
      document.querySelector(".qs-timer-overlay").style.opacity = (document.querySelector(".qs-timer-overlay").style.opacity * 1) + 0.1;
      if (document.querySelector(".qs-timer-overlay").style.opacity * 1 == 1) {
        clearInterval(timer);
      }
    }, 1000);
    .qs-main-header .qs-timer {
      padding: 13px 10px;
      min-width: 130px;
      text-align: center;
      display: inline-block;
      background-color: #dd8b3a;
      color: #FFF;
      font-size: 20px;
      border-radius: 50px;
      text-transform: uppercase;
      float: right;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      opacity: 0.99; /* creates a separate stacking context */
    }
    .qs-main-header .qs-timer-overlay {
      z-index: 1;
      width: 10%;
      max-width: 100%;
      position: absolute;
      height: 100%;
      top: 0;
      left: 0;
      background-color: #c7543e;
      opacity: 0.0;
      /* border-radius: 50px 50px 0px 50px; */
    }
    .qs-main-header .qs-timer-content {
      z-index: 2;
      position: relative;
    }
    .scale-transition {
      -webkit-transition: all 1s;
      transition: all 1s;
    }
    <div class="qs-main-header">
      <div class="qs-timer scale-transition ng-hide" ng-show="visibility.timer">
        <div class="scale-transition qs-timer-overlay"></div>
        <div class="qs-timer-content ng-binding">0 <span class="ng-binding">Sec(s)</span>
        </div>
      </div>
    </div>
  • Adding a transform to the element. I have used translateZ(0px) in the below snippet as this also doesn't create any visual difference.

    var timer = setInterval(function() {
      document.querySelector(".qs-timer-overlay").style.opacity = (document.querySelector(".qs-timer-overlay").style.opacity * 1) + 0.1;
      if (document.querySelector(".qs-timer-overlay").style.opacity * 1 == 1) {
        clearInterval(timer);
      }
    }, 1000);
    .qs-main-header .qs-timer {
      padding: 13px 10px;
      min-width: 130px;
      text-align: center;
      display: inline-block;
      background-color: #dd8b3a;
      color: #FFF;
      font-size: 20px;
      border-radius: 50px;
      text-transform: uppercase;
      float: right;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transform: translateZ(0px) /* creates a separate stacking context */
    }
    .qs-main-header .qs-timer-overlay {
      z-index: 1;
      width: 10%;
      max-width: 100%;
      position: absolute;
      height: 100%;
      top: 0;
      left: 0;
      background-color: #c7543e;
      opacity: 0.0;
      /* border-radius: 50px 50px 0px 50px; */
    }
    .qs-main-header .qs-timer-content {
      z-index: 2;
      position: relative;
    }
    .scale-transition {
      -webkit-transition: all 1s;
      transition: all 1s;
    }
    <div class="qs-main-header">
      <div class="qs-timer scale-transition ng-hide" ng-show="visibility.timer">
        <div class="scale-transition qs-timer-overlay"></div>
        <div class="qs-timer-content ng-binding">0 <span class="ng-binding">Sec(s)</span>
        </div>
      </div>
    </div>

The first two approaches are more preferable than the third because the third one works only on a browser that supports CSS transforms.

like image 182
Harry Avatar answered Oct 07 '22 10:10

Harry


Yes, adding opacity: 0.99; to .qs-timer issue will fixed.

When opacity: 1 OR NOT define:
In this special case, there is no transparency involved so that gfx could avoid doing the expensive things.

In case Opacity: 0.99:
nsIFrame::HasOpacity() decides that there is an opacity, so gfx include valuable things. ( likes opacity with border-radius)

For more help Special case opacity:0.99 to treat it as opacity:1 for graphics , This ticket is not providing the opinion of our actual goal, but giving the idea about what is happening inside of CSS.

like image 21
Sachink Avatar answered Oct 07 '22 09:10

Sachink