Thursday 3 April 2014

CSS: Set Opacity of background image without affecting child elements

Fixing Parent-Child Opacity

Parent/Child signOne of the posts on this website that consistently gets a significant amount of traffic (5000+ page views this month alone) is a ridiculous article I wrote that discusses how to make a child element not inherit the opacity setting of its parent.
As we all know, CSS transparency that uses the opacity property can be annoying in this area.
Basically, if a parent element has an opacity value set at, say, 0.5, all of its children will inherit that opacity setting, and there’s no way to reverse that opacity on the child elements.
That post I wrote discussed a hacky workaround where you actually remove the element from its parent, position it absolutely, then move it so it still appears in the same place it did when it was a child.
If you know anything about CSS positioning, then you know why this is not a great solution.
Nonetheless, I still get lots of traffic coming to that article not only from Google searches, but also from various Stack Overflow threads that link to it.
So I decided to write a script that fixes this issue. But first, let’s settle a few things.

The Best Way to Resolve This Issue

I’m guessing that in more than 90% of cases, this is pretty much a non-issue. If you need transparency on a parent element, you can do it using a few different methods that avoid this parent-child opacity issue:
  • If it’s only a background color on the parent, use RGBA or HSLA
  • If it’s a background image, use a transparent PNG
  • If you’re just desiring a “washed out” look for a color or background image or pattern, do it in your image editor, or sample the washed-out color you want and insert it with Hex or RGB in your CSS
But hey, it’s fun to write polyfills and workarounds for these types of problems, and it does seem like this sort of thing is in demand, even if most developers are approaching the problem in the wrong way to begin with.

Introducing thatsNotYoChild.js

I realized that the workaround to get the child elements out of their parent and repositioned is not that crazy. So I wrote a script that does this exact thing automatically, but it’s much more effective than that original solution.
Here’s an embedded pen demonstrating thatsNotYoChild.js in action:
HTML:
<div class="parent" id="parent">

  <div class="child">
  </div>
  <p>The blue box and the text are child elements of the element that has a photo background. The child elements do not inerit the opacity setting on the parent. This is done using a jedi mind trick, aka "JavaScript".</p>
 
  <p>The rest of this is just filler text. Don't read this, you're just wasting your time. Are you serious? You're still reading? I'm not lying, this is just filler. I just need this to wrap below the box. That's all.</p>
</div>

CSS:
* {
  box-sizing: border-box; 
}

.parent {
  width: 500px;
  background-image: url(http://www.impressivewebs.com/demo-files/css-opacity/bicycle.jpg);
  border: solid 3px black;
  margin: 0 auto;
  padding: 30px;
  opacity: .2;
            filter: alpha(opacity=20);
}
 
  .child {
    width: 150px;
    height: 150px;
    background: blue;
    float: left;
    margin: 0 20px 10px 0;
    border: solid 5px yellow;
    box-shadow: rgba(0, 0, 0, .2) 4px 4px 5px;
  }

p { margin: 0 0 20px 0; }

Javascript:
/*
ThatsNotYoChild.js, by Louis Lazaris
Explanation: http://www.impressivewebs.com/fixing-parent-child-opacity/
This is a hacky workaround to let you use opacity on any element and prevent the child elements from inheriting the opacity.
Works in IE8+.
If anyone can get line 23 working in IE7, it will be fully cross-browser.
*/
function thatsNotYoChild(parentID) {

    var parent           = document.getElementById(parentID),
        children         = parent.innerHTML,
        wrappedChildren  = '<div id="children" class="children">' + children + '</div>',
        x, y, w, newParent, clonedChild, clonedChildOld;

    parent.innerHTML = wrappedChildren;
    clonedChild = document.getElementById('children').cloneNode(true);
    document.getElementById('children').id = 'children-old';
    clonedChildOld = document.getElementById('children-old');
    newParent = parent.parentNode;

    newParent.appendChild(clonedChild);
    clonedChildOld.style.opacity = '0';
    clonedChildOld.style.filter = 'alpha(opacity=0)';

    function doCoords () {
      x = clonedChildOld.getBoundingClientRect().left;
      y = clonedChildOld.getBoundingClientRect().top;
      if (clonedChildOld.getBoundingClientRect().width) {
        w = clonedChildOld.getBoundingClientRect().width; // for modern browsers
      } else {
        w = clonedChildOld.offsetWidth; // for oldIE
      }

      clonedChild.style.position = 'absolute';
      clonedChild.style.left = x + 'px';
      clonedChild.style.top = y + 'px';
      clonedChild.style.width = w + 'px';
    }

    window.onresize = function () {

      doCoords();

    };
 
    doCoords();

}

// call the function and pass the ID of the parent that has opacity set.

thatsNotYoChild('parent');
Go ahead, change the markup to anything you want inside the #parent element; it should work for anything you put in there.
You can view the code in the embedded CodePen, and here’s a step by step description of what it does:
  • Grab all child elements of the element that has the opacity setting, wrap them in a <div>
  • Use cloneNode to clone the newly-wrapped child group
  • Place the new clone outside the parent element
  • Change the ID of the original group
  • Set the opacity of the original group to zero (you can reduce the opacity of the children but you can’t raise it)
  • Use getBoundingClientRect() (which works everywhere that’s relevant) to find the exact position and width of the original child group
  • Use element.style to absolutely position and size the clone group using the values obtained from getBoundingClientRect()
  • Use window.onresize to run the previous two steps every time the window is resized.
Compared to the old article I wrote that fixed this issue with a CSS-only solution, this solution has a few advantages. First, although the child elements are absolutely positioned, taking themout of the normal flow, the space the child elements occupy is still occupied by the original child group, which isn’t visible due to having its opacity set to 0. The cloned group overlays the same space, making it appear as if it never moves, and the other elements on the page don’t reposition themselves since they are subject to the positioning of the original, now invisible, child group.
The other advantage is that this solution doesn’t require any changes to the markup, whereas that other CSS-only solution required that you change the markup.
To use the script, just call the function like this, passing in the ID of the parent element that has opacity set:
  1. thatsNotYoChild('parent');  

Feedback?

I don’t know too much about the different DOM methods I used in this script. I’m guessing for example that window.onresize is not great for performance and repaints.
This was, more or less, a fun little hacky script that’s not too heavy so maybe someone will find it useful, assuming there are no major performance issues with it.
If you have any feedback on improvements to the code, or see any potential bugs or drawbacks, feel free to comment and/or fork it.



CSS opacity only to background color not the text on it?

It sounds like you want to use a transparent background, in which case you could try using the rgba()function:

rgba()

Colors can be defined in the Red-green-blue-alpha model (RGBa) using the rgba() functional notation. RGBa extends the RGB color model to include the alpha channel, allowing specification of the opacity of a color.
a means opacity: 0=transparent; 1=opaque;
rgba(255,0,0,0.1)    /* 10% opaque red */  
rgba(255,0,0,0.4)    /* 40% opaque red */  
rgba(255,0,0,0.7)    /* 70% opaque red */  
rgba(255,0,0,  1)    /* full opaque red */ 
Sample usage:
#div {
    background: rgb(54, 25, 25); /* Fall-back for browsers that don't
                                    support rgba */
    background: rgba(54, 25, 25, .5);
}

I've never seen that as "overriding" or "underriding". It's a matter of relative opacities. If the parent has an opacity of 0.5, the child has it too (in relation to the parent's stacking context). The child can have its own opacity value between 0 and 1, but it will always be relative to the parent's opacity. So if the child also has opacity: 0.5 set, it will be 0.25 the opacity of some of the parent's sibling with opacity 1.
The spec treats it as an alpha mask, where opacity can only be removed. An element is either opaque, or has some degree of transparency (anything < 1):
Opacity can be thought of as a postprocessing operation. Conceptually, after the element (including its descendants) is rendered into an RGBA offscreen image, the opacity setting specifies how to blend the offscreen rendering into the current composite rendering.
and later on:
If the object is a container element, then the effect is as if the contents of the container element were blended against the current background using a mask where the value of each pixel of the mask is <alphavalue>
As for why it was implemented that way, I don't think it was intentional in the sense of "let's forbid that". Maybe this approach was chosen for being simpler to calculate, and only later an actual need for something different was recognized (then rgba color and background-color were introduced – and I may be wrong about the timeline here).