Why does transitioning the box shadow cause a full page repaint? - html

I noticed that my page was lagging when I hovered over an element with an animated box-shadow. Using Chrome's Devtools, I noticed that the entire page was being repainted when I hovered over the element. The repaint was taking 40+ milliseconds, or about 3 frames. The transition lasts about half a second, so there's noticeable lag during the half second.
How do I limit the repaint to just the area with the box shadow?
Here's a demo: http://jsfiddle.net/8sa41xfL/
html,body{
height:100%;
}
#test{
background:red;
height:100px;
width:200px;
transition:box-shadow 0.5s;
}
#test:hover{
box-shadow:0 0 3px 3px rgba(0,0,0,0.3);
}
<div id=test></div>
transform:translateZ(0) doesn't work on my page, but it works in the fiddle. Is there another fix aside from transform:translateZ(0)?

As mentioned in the thread linked in Pierre's answer box-shadow are expensive to paint. Explaining why it is expensive would require in-depth understanding of the way rendering works and I don't have near enough knowledge to explain it completely. But this answer attempts to explain why the whole page gets repainted and the various possible methods to avoid it.
According to CSS Triggers website:
Changing box-shadow does not trigger any geometry changes, which is good. But since it is a visual property, it will cause painting to occur. Painting is typically a super expensive operation, so you should be cautious.
Once any pixels have been painted the page will be composited together.
Why does the whole page get repainted everytime?
The below articles explain the way that painting actually works at a high level:
HTML5 Rocks - How Browsers Work - Painting
The Chromium Project - GPU accelerated rendering in Chrome
Based on those articles, we can see that each node in the DOM tree that produces a visual output is considered as a RenderObject and that each RenderObject is part of a RenderLayer directly or indirectly. Whenever a change happens, the renderer (or the render object) invalidates its rectangle (or RenderLayer) on screen and triggers a repaint.
In this case it seems like the whole page is getting repainted because the #test element does not warrant the creation of a separate RenderLayer (based on the criteria mentioned in the Chromium Project article) and so becomes a part of the root render layer. Because it is a part of the root render layer the whole page is getting repainted everytime a repaint is required.
The following snippet proves that the above assertion is correct. Here, I have added a #cover element (with positioning) to enclose the #test element. Now since the #cover element has explicit positioning, it creates an extra layer above root layer and #test becomes a part of this intermediate layer. Now, we can see that the box-shadow transition repaints only this intermediate layer and not the whole page.
html,
body {
height: 100%;
}
#cover {
position: relative;
}
#test {
background: red;
height: 100px;
width: 200px;
transition: box-shadow 0.5s;
}
#test:hover {
box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.3);
}
<div id=cover>
<div id=test></div>
</div>
What is the solution?
There are various CSS properties that can be used to address this problem but they all seem to point to the same point at a high level - which is, to create a separate render layer for the #test element.
Below are a few possible options to create a separate render layer for the #test element:
By adding explicit position properties - This is the same option described in Pierre's answer but absolute positioning is not the only option. Even relative positioning would solve it.
html,
body {
height: 100%;
}
#test {
position: relative;
background: red;
height: 100px;
width: 200px;
transition: box-shadow 0.5s;
}
#test:hover {
box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.3);
}
<div id=test></div>
By adding transparency (opacity) - Browsers seem to treat even opacity: 0.99 as adding transparency and it is very useful because adding this doesn't cause any visual difference.
html,
body {
height: 100%;
}
#test {
background: red;
height: 100px;
width: 200px;
opacity: 0.99;
transition: box-shadow 0.5s;
}
#test:hover {
box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.3);
}
<div id=test></div>
By adding a dummy CSS filter - We could add a filter: blur(0px) as it would do nothing.
html,
body {
height: 100%;
}
#test {
background: red;
height: 100px;
width: 200px;
-webkit-filter: blur(0px);
filter: blur(0px);
transition: box-shadow 0.5s;
}
#test:hover {
box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.3);
}
<div id=test></div>

CSS box shadows are expensive to paint. Read more on SO here.
If you want to avoid a full page repaint, use a position:absolute on your element. This will repaint the area that surrounds your element without affecting the whole page. Fiddle.

Related

How can I use Chrome to measure pixel distance between elements?

Titles says it all. Is it possible to do without any extensions?
Use a Chrome Extension, this one works great for checking pixel distance between any web elements.
https://chrome.google.com/webstore/detail/dimensions/baocaagndhipibgklemoalmkljaimfdj?hl=en-US
Hope this helps
I was struggling with this and found an answer in devtools. First, in responsive mode, to the far right in the header there is a More options menu that has a Show rulers option. Select that. Then in the details settings under Elements, there is a Show rulers option to check. With both of those, selecting an element will show lines extended from the rulers to the element so you can see their location. You can see begin and end for various elements to calculate spacing between. I needed to handle spacing to edge so it was a little easier.
I don't know how to turn on the rulers when not in responsive mode, but they remained when I went back to a web layout.
Would this approach work? Get hold of an image containing alternating black and white dots(like a chess board) where each dot is 1 px. Make it the background image. You can zoom in like around 1000% and count the number of dots.
One thing you could do is using the Console tab to compute the horizontal or vertical distance between two elements using dimension/position properties/methods such as Element.getBoundingClientRect() or HTMLElement.offsetTop, but I guess you are looking for something that works more like a tool rather than coding your own solution.
Another maybe more usable option would be to use the Elements > Styles panel to add some kind of visual effect incrementally that allows you to measure what you need. For example, you could add a box-shadow / outline to an element and increment its size pixel by pixel until it touches the element next to it, so that you know how many pixels separate them.
Here's a simple code example / "demo" so that you see exactly what I mean:
html,
body {
width: 100vw;
height: 100vh;
margin: 0;
}
div {
position: absolute;
display: flex;
border: 3px solid black;
top: 10px;
bottom: 10px;
width: 100px;
box-sizing: border-box;
}
.a {
left: 10px;
}
.b {
left: 120px;
animation: measure 2s linear 0s infinite alternate;
}
#keyframes measure {
0%, 10% { box-shadow: 0 0 0 0px red; }
10.001%, 20% { box-shadow: 0 0 0 2px red; }
20.001%, 30% { box-shadow: 0 0 0 3px red; }
30.001%, 40% { box-shadow: 0 0 0 4px red; }
40.001%, 50% { box-shadow: 0 0 0 5px red; }
50.001%, 60% { box-shadow: 0 0 0 6px red; }
60.001%, 70% { box-shadow: 0 0 0 7px red; }
70.001%, 80% { box-shadow: 0 0 0 8px red; }
80.001%, 90% { box-shadow: 0 0 0 9px red; }
90.001%, 100% { box-shadow: 0 0 0 10px cyan; }
}
<div class="a">A</div>
<div class="b">B</div>
Other than that, your best option is to use an Extension.
I would recommend Dimensions, a Chrome Extension that will constantly and automatically measure vertical and horizontal space as you move the cursor until it finds an "obstacle", which is way faster and easier than drawing a box to take a measurement, as most of the other measurement/ruler extensions do.
You can use firefox developer tools as it supports measuring pixels and distance out of the box. It does not need any external plugin.
Please check here

transparent png not rendering as transparent?

I saved the image as a transparent png but nothing I can do seems to fix it!?! Any ideas???
It should look like this:
Here is a copy of the image in GIMP showing that it's indeed transparent:
finally, Some good old code:
The Markup:
<form class="search" action="search.php"><input class="search" type="text" name="search" id="searchbox"/></form>
Search Box CSS:
.search, .search:active, .search:focus, .search:visited {
position: absolute;
color: #fff;
top: 3px;
width: 368px;
right: 9%;
font-size: 28px;
z-index: 3;
border-radius: 20px;
/* box-shadow: inset -2px 0px 1px rgba(0,0,0,.8); */
text-indent: 10px;
text-shadow: 0px -2px 3px rgba(0, 0, 0, .7);
background-color: #00D4C7;
}
The Search icon css itself:
Pseudo ::before element
.search:before {
content: "";
position: absolute;
top: 7px;
left: 268px;
background-image: url("images/icon-search.png");
background-color: rgb(0, 185, 171);
width: 46px;
height: 30px;
z-index: 4;
}
Note: If I remove the class search from form, It removes my image, if I remove class search from the input element it still renders with that funky shade over my image...Any ideas?
Edit 1: If I do as suggested by setting the explicit dimensions of the image (as I did for other pseudo elements with no problem) it does not resolve my issue. I've already submitted the project so at this point it's a matter of me wanting to know what happened and how I can fix this. I resorted to a css hack that changed the brightness to a closer match [with a faint outline still 😞 ]
Edit 2: Show me the JS Fiddle!
you did give your image a background-color: rgb(0, 185, 171); what is #00b9ab
and the searchbox background-color: #00D4C7; what is rgb(0, 212, 199)
My image itself was not truly transparent. There is is a small opacity channel that causes that grey haze to appear on a non-white background. I caught wind of this when posting to imgur....and confirmed it for sure when I actually made a copy of the layer (in photoshop) using select by color.
Solution: Check your images...to do this, load it onto a window by itself and set the html body to a non-white color. Wish I thought to do this before submitting this:
This might be happening because your background image size is different than its container's, .search::before, size. And/or because your .search::before background-color is a different hex value.
Try this:
Add background-color: transparent; and background-size: 46px 30px; to .search::before. This will make it so that if your background image is smaller than the container, the rest of the space will be transparent and set the background image size to be the same as its container, which you have explicitly set.
I found that the images that I was getting from the internet were not truly transparent. This website enabled me to get it to work. https://www.remove.bg/ (the image I used had the checkered background to indicate it was transparent, it just didn't work in the img tag until I used that website.)

Rotate css animation glitchy in firefox

I am trying to rotate a div with this dead-simple code:
<!DOCTYPE html>
<html>
<head>
<style>
.spinner {
width: 100px;
height: 100px;
margin: auto;
border-radius: 50%;
border-bottom: 1px solid #f00;
animation: rotate-bottom 1s linear infinite;
}
#keyframes rotate-bottom {
from {
transform: rotateX(30deg) rotateY(-60deg) rotateZ(0deg);
}
to {
transform: rotateX(30deg) rotateY(-60deg) rotateZ(360deg);
}
}
</style>
</head>
<body>
<div class="spinner"></div>
</body>
</html>
I created a jsfiddle using the code above: http://jsfiddle.net/zg8vdyns/1/
Everything works fine on Chrome and Internet Explorer. A red curved line rotates in an endless, smooth and steady loop. However, firefox (39.0) seems to have issues rendering the animation (both the windows and linux build). First, the spinning line is much shorter than it should be. Second, the animation keeps faltering intermittently (it is not smooth). This looks like a firefox bug. Does anyone have a deeper insight into this issue?
Btw I know I should probably prefix 'animation' and 'keyframes' with '-moz-' but that is not the issue here.
Your issue is half-pixel/sub-pixel rendering. Playing around and changing border-bottom: 1px solid #f00; to border-bottom: 3px solid #f00; shows that animation is ok, but the rendering is very different from other browser engines... From another answer here of StackOverflow: Firefox CSS Animation Smoothing (sub-pixel smoothing)
The rendering engines for each browser is obviously different. Firefox does not implement an anti-aliasing effect on CSS animations. This does not inherently make it better or worse, it just depends on what you are animating. Linear transitions can appear undesirably blurred in Chrome for example.
That said it appears what you would like to achieve is to have an anti-aliased/sub-pixel smoothed transitions. We can't change the way the engine renders but we can manipulate the animation to appear softer to the end user.
But, differently from the approach provided by the answer in the link, in your scenario I think that there is a easier way to make the rendering more equivalent: http://jsfiddle.net/zg8vdyns/7/
Adding border-left: 1px solid rgba(255, 0, 0, 0.7); will kinda "force" the rendering of the half-pixels/sub-pixels that FireFox doesn't naturally...
Update:
#joshin855 also give a great answer below: adding the property background:rgba(255,255,255,0.01); will kinda "force" the rendering of the half-pixels/sub-pixels too. Your solution is very nice... It only have the disadvantage of a filled circle which depending on the scenario may not be suitable, but the line animation seems even more equivalent than in my solution... So, it also may be a good solution.
As far the line being a dot you can add background:white; or background:RGBA(255,255,255,.01); to the element which should fix the problem and make it look similar to other browsers. Sorry it's not a great answer just thought I would throw in my 2 cents.
.spinner {
width: 100px;
height: 100px;
margin: auto;
border-radius: 50%;
border-bottom: 1px solid #f00;
animation: rotate-bottom 1s linear infinite;
background:RGBA(255,255,255,.01);
}
http://jsfiddle.net/fqqko0uv/2/

Chrome bug - border radius not clipping contents when combined with css transition

My issue is that during CSS transition border-radius temporarily stops clipping elements inside if transition of overlapping element involves transform. In my case I have two divs absolutely positioned one above the other where the first one has transition triggered by action on clicking a navigation element inside the second one, like:
<div id="below"></div>
<div id="above"><div id="nav"></div></div>
The above div has border-radius: 50% and clips the nav div. In CSS it goes like (minimal example, original onclick action illustrated as :hover):
#below {
position: absolute; width: 250px; height: 250px;
-webkit-transition: all 1s linear;
transition: all 1s linear;
}
#below:hover {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
#above {
position: absolute;
width: 200px; height: 200px;
border-radius: 50%;
overflow: hidden;
}
#nav {
width: 40px;
height: 200px;
background-color: rgba(0,0,0,0.3);
}
Of course it is better visible in http://jsfiddle.net/UhAVG/ with some additional styling for better illustration.
This works as expected in IE10+ and FF25, also in Chrome 31 and 32 with hardware acceleration disabled. In result only accelerated Chrome shows this unwanted behaviour. So I'm wondering if it's possible to workaround it somehow using current CSS3 techniques.
After some more experiments I've finally found the solution. Sometimes simple ones are the hardest to find. In this case #above {z-index: 1;} (like in http://jsfiddle.net/UhAVG/1/) solves the issue. Wild guess is that z-index prevents some optimization that combines operations from single layer and doing so mistakenly optimizes out applying border-radius on element. With layers separated this is no longer the case.

CSS3 - How to "restore" ::-webkit-scrollbar property to the default scroll bar

Hi I'm using the next css code to style scroll bars in Safari and Chrome. And works really great but I´m facing the next issue, I would like te restore the default value, when I view the site on my ipad. I'm using #media css for achived this but, I don't know how to restore the defaults values.
#media screen and (min-width: 768px) and (max-width: 1024px) { }
/*Scroll bar nav*/
::-webkit-scrollbar {
width: 12px;
}
/* Track */
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
-webkit-border-radius: 10px;
border-radius: 10px;
background:#FFF;
}
/* Handle */
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
background: rgba(204,204,204,0.8);
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(204,204,204,0.4);
}
UPDATE 2022
I answered this almost 10 years ago and seems like after 2021 this solution stop working, read the solution from #Volomike, it might get you where you want to.
I just realized you can set all the properties in auto; and will do the trick. This is a self answer but I guess someday someone can have the same question.
/*Scroll bar nav*/
::-webkit-scrollbar {
width: auto;
}
/* Track */
::-webkit-scrollbar-track {
-webkit-box-shadow: auto;
-webkit-border-radius: auto;
border-radius: auto;
background:auto;
}
/* Handle */
::-webkit-scrollbar-thumb {
-webkit-border-radius:auto;
border-radius:auto;
background:auto;
-webkit-box-shadow:auto;
}
::-webkit-scrollbar-thumb:window-inactive {
background: auto;
}
I don't know if exist another method.
-- UPDATE --
Look like you can also use the initial and unset value
//reverting all the values
::-webkit-scrollbar {
all:unset;
}
or apply to an specific one {width : unset} || {width : initial}
NOTE: Using unset will not work on IE11
Use the initial value or unset value for the properties you want to revert (depending on how exactly you want to revert them).
Both these values can be applied to all CSS properties.
example
::-webkit-scrollbar {
width: initial;
}
or
::-webkit-scrollbar {
width: unset;
}
If you want to revert all properties of a rule then you should use the all keyword
example
::-webkit-scrollbar-thumb {
all:unset;
}
Notice: No IE support for any of these as of yet.
Varying levels of support for each browser (see the linked docs for details)
I had trouble with this. I don't know what exactly triggers the latest Chrome to switch scrollbars on a desktop browser to overlay mode, but it was unnerving to me because it makes the page scroller look broken to an inexperienced user. The selected answer didn't seem to work in my version of Chrome on Lubuntu Linux 20.04.1, version 100.0.4896.127. So, over several hours, I painstakingly recreated the settings to Chrome's system defaults and yet something that works in both light and dark mode too. Note I'm only styling the BODY element's vertical scroller in this example, but you can adapt easily for horizontal scrollers as well as not just apply it to the BODY element.
BODY::-webkit-scrollbar
{
all:unset;
}
BODY::-webkit-scrollbar-button
{
display:block;
background-color:ButtonFace;
box-shadow:inset 0px 0px 0px 20px rgba(255,255,255,0.3);
height: auto;
width: initial;
background-position: center 5px;
background-size:9px 7px;
image-rendering: pixelated;
background-repeat:no-repeat;
}
BODY::-webkit-scrollbar-button:hover
{
box-shadow:inset 0px 0px 0px 20px rgba(128,128,128,0.5);
}
BODY::-webkit-scrollbar-button:active
{
box-shadow:inset 0px 0px 0px 20px rgba(128,128,128,0.7);
}
BODY::-webkit-scrollbar-button:vertical:start:increment,
BODY::-webkit-scrollbar-button:vertical:end:decrement
{
display:none;
}
BODY::-webkit-scrollbar-button:vertical:decrement
{
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='ButtonText'><polygon points='50,00 0,50 100,50'/></svg>");
}
BODY::-webkit-scrollbar-button:vertical:increment
{
background-position: center 6px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='ButtonText'><polygon points='0,0 100,0 50,50'/></svg>");
}
BODY::-webkit-scrollbar-thumb
{
background-color:ButtonFace;
box-shadow:inset 0px 0px 0px 20px rgba(128,128,128,0.3);
border-left:2px solid rgba(255,255,255,0.3);
border-right:2px solid rgba(255,255,255,0.3);
}
BODY::-webkit-scrollbar-thumb:hover
{
box-shadow:inset 0px 0px 0px 20px rgba(128,128,128,0.5);
}
BODY::-webkit-scrollbar-thumb:active
{
box-shadow:inset 0px 0px 0px 20px rgba(128,128,128,0.7);
}
BODY::-webkit-scrollbar-track
{
background-color:ButtonFace;
box-shadow:inset 0px 0px 0px 20px rgba(255,255,255,0.3);
}
Extra Notes:
Using the ButtonFace and ButtonText colors, you can have the scrollbar react to light and dark mode. However, that doesn't give you the varying levels of light and dark you need on the control. You'd think perhaps you could use a filter:brightness(x); level or an opacity:x level to create your varying levels of light and dark on the control -- but those oddly don't work on these scrollbars. Instead, I found that that an inset box-shadow set on wide spread, and nothing else, with rgba colors, worked well.
I found that setting width:initial, plus height:auto, on the button alone was enough to set the width of the scrollbar control parts.
I got the triangle SVGs from here. Note that I changed the fill on those to ButtonText in order to make it work in browser dark and light modes.
Note that I used rgba(128,128,128,x) in some cases because starting from white or black with opacity created odd issues when switching light and dark modes, and so I chose the middle value with opacity to get around that.
The image-rendering:pixelated was very useful because otherwise the very tiny up and down triangle SVG icon would be antialiased so much that it would look too opaque. This allowed the SVG icon to maintain its darkness on light mode, and brightness on dark mode.
I filed this issue with the Chromium browser team to see if they can give us some reliable CSS to turn off overlay scrollbars on a given element, should we need that.
https://bugs.chromium.org/p/chromium/issues/detail?id=1333947
EDIT: Recently, I found that web server cache has something to do with the Google Chrome/Chromium/Edge Chromium browsers from somewhere around version 100+ at least here around 6/17/2022. When I turn on web server cache, any sub-page on a website going back to a previous page (as a regular relative link, not as a history.back(); call), will consistently show an overlay scrollbar instead of a fixed scrollbar. But if I force the page with an .htaccess rule that is a website cache buster, then the problem goes away and I see a scrollbar like normal.
As commenter vsync mentioned, once you've hid the scrollbar, if you add a new css rule to show the scrollbar, it doesn't work as expected.
Can be solved with JS
let styles = document.getElementsByTagName('style');
for(let i = 0; i < styles.length; i++) {
let style = styles[i];
let rules = style.sheet.cssRules;
for(let r = 0; r < rules.length; r++) {
let rule = rules[r];
if(rule.selectorText === '::-webkit-scrollbar') {
style.sheet.deleteRule(r);
}
}
}