How can I smoothly transition CSS background images?

10,959

Solution 1

I think I have a solution!  (sorry, just very exited I got this working:)

//library (minified)
this.BgImgFader=function(){var styleRules;function getArray(str){if(str.indexOf(',')==0){str.substring(1);}if(str.lastIndexOf(',')==str.length-1){str.substring(0,str.length-1);}if(str.indexOf(',')==-1){var selectors=[str];}else{var selectors=str.split(',');}for(var i=0;i<selectors.length;i++){selectors[i]=selectors[i].trim();}return selectors;}function getStyleSheet(style){if(typeof style==='number'){return document.styleSheets[style];}else{for(var i=0;i<document.styleSheets.length;i++){var file=document.styleSheets[i].href;file=file.substring(file.lastIndexOf('/')+1);if(file.toLowerCase()==style.toLowerCase()){return document.styleSheets[i];}}}}function addStyleRule(sheet,selector,declarations){if(sheet.addRule){sheet.addRule(selector,declarations);}else{sheet.insertRule(selector+'{'+declarations+'}');}}this.prepare=function(style,selectors){var selectors=getArray(selectors);var styleSheet=getStyleSheet(style);for(var i=0;i<selectors.length;i++){addStyleRule(styleSheet,selectors[i],'position:relative;');addStyleRule(styleSheet,selectors[i]+'::after','position:absolute;top:0;right:0;bottom:0;left:0;opacity:0.0;content:"";');}};this.fade=function(style,selectors,global,opacity,endOpacity,delta){var selectors=getArray(selectors);var styleSheet=getStyleSheet(style);styleRules=styleSheet.rules?styleSheet.rules:styleSheet.cssRules;fadeOpacity(selectors,global,opacity,endOpacity,delta,[]);};function fadeOpacity(selectors,global,opacity,endOpacity,delta,rules){opacity+=delta;if(rules.length==0){for(var i=0;i<selectors.length;i++){for(var j=0;j<styleRules.length;j++){if(global&&styleRules[j].selectorText.toLowerCase().indexOf(selectors[i].toLowerCase()+'::after')!=-1){rules.push(styleRules[j]);}else if(styleRules[j].selectorText.toLowerCase()==selectors[i].toLowerCase()+'::after'){rules.push(styleRules[j]);break;}}}}for(var i=0;i<rules.length;i++){rules[i].style.opacity=opacity;}if(opacity<endOpacity){setTimeout(function(){fadeOpacity(selectors,global,opacity,endOpacity,delta,rules);},0);}else{for(var i=0;i<rules.length;i++){rules[i].style.opacity=endOpacity;}rules.length=0;}}};

//instantiate BgImgFader in global domain
var BgImgFader = new BgImgFader();

window.onload = function(){
  //prepare specified elements
  BgImgFader.prepare(0, '.exampleClass'); //style, selectors

  //fade specified elements
  BgImgFader.fade(0, '.exampleClass', true, 0, 0.5, 0.002); //style, selectors, global, startOpacity, endOpacity, delta
};
#exampleId {
  width: 300px;
  height: 200px;
  margin: 10px 0px 0px 10px;
  background-color: #AAAAAA;
}
#exampleId .exampleClass {
  width: 200px;
  height: 130px;
  padding: 5px;
}
#exampleId .exampleClass::after {background:url(https://placeimg.com/640/480/any) center/cover no-repeat;}
<div id="exampleId">
  Some other text to illustrate how this can be implemented.
  <div class="exampleClass">
    I assume you want transparent background images because you have text in the element that you do want to show from the start?
  </div>
</div>
codepen: https://codepen.io/anon/pen/QMLPbr

I basically ended up building a tiny library. Pure JavaScript, no jQuery required.


All you have to do is add these three BgImgFader lines in JS:

//instantiate BgImgFader in global domain
var BgImgFader = new BgImgFader();

window.onload = function(){
  //prepare specified elements
  BgImgFader.prepare(0, '.exampleClass'); //stylesheet, selectors

  //fade specified elements
  BgImgFader.fade(0, '.exampleClass', true, 0, 0.5, 0.002); //stylesheet, selectors, global, startOpacity, endOpacity, delta
};

And add the following to all your background-image-elements in CSS:

#example {...}
#example::after {background:url(path/to/image.png) center/cover no-repeat;}

So for every element with a background-image, you have to add the #example::after { rule and place your background-image in there. (You do NOT have to do this in HTML, only in CSS.)


I've added the library's source code as a code snippet below. Comments are in the code.
You can either paste the code at the top of your own script, or put the code in a file and in your HTML, link to that file like you would to any other library (before your own code):

<script type="text/javascript" src="path/to/bgimgfader.js"></script>

/**
 * BgImgFader - Library:
 * This library makes it possible to fade the background-image of an element.
 * The image can be faded in or out.
 * 
 * Compatibility:
 * - IE9 and higher should be fine, lower could give trouble.
 * - Older versions of FF/Chrome will probably give some problems too, but I think we can safely assume 
 *   that those who chose either one of these browsers, did so because they choose NOT to live in the past..
 * - Opera and others... I have absolutely no idea.
 * 
 * #################################################################################
 * INSTRUCTIONS---------------------------------------------------------------------
 * 1. In CSS:
 *    a. For every element with a background-image, create an '::after' rule, and put the image in there:
 *       (You don't have to create these '::after'-elements in the HTML)
 * 
 *          #element {...}
 *          #element::after {background:url(path/to/image.png) center/cover no-repeat;}
 * 
 *       The following declarations will be added by the BgImgFader, keep that in mind:
 *          #element {position:relative;}
 *          #element::after {position:absolute; top:0;right:0;bottom:0;left:0; opacity:0.0; content:"";}
 * 
 *          (The important one is 'position:relative;', the ones on the '::after'-element have no consequences) 
 * 
 * ---------------------------------------------------------------------------------
 * 2. In JavaScript:
 *    a. Instantiate the BgImgFader in the global domain:     var BgImgFader = new BgImgFader();
 * 
 * 
 *    b. Prepare the elements with a background-image:     BgImgFader.prepare(0, 'elements'); //style, selectors
 * 
 *       - style: Reference to the style sheet with the rules for the specified elements.
 *                This can be either an INTEGER for internal style sheets (0), 
 *                or a STRING of a filename for external style sheets ('style.css').
 *       - selectors: STRING reference to the selectors in the style rules.
 *                    This works the same as in the CSS, below a few examples.
 *                         Individual tags:     ('div')	        ('#id')          ('.class')
 *                           Multiple tags:     ('div.class')   ('#id .class')   ('.class.subclass')
 *                      Multiple selectors:     ('div, #id, div.class, #id .class, .class.subclass')
 * 
 * 
 *    c. Initiate the fade:     BgImgFader.fade('style.css', 'elements', true, 0, 0.5, 0.005); //style, selectors, global, startOpacity, endOpacity, delta
 * 
 *       - style: See 2b for the details.
 *       - selectors: See 2b for the details.
 *       - global: BOOLEAN that deternimes whether only complete matches for the selectors are allowed, 
 *                 or partial matches as well, increasing the range of the BgImgFader.
 *                 TRUE allowes partial matches: feed the BgImgFader '.class' and it will also try to fade 'div .class'.
 *                 FALSE allowes only complete matches.
 *       - startOpacity: FLOAT that indicates the start opacity (0.0  -  1.0).
 *       - endOpacity: FLOAT that indicates the end opacity (0.0  -  1.0).
 *       - delta: FLOAT that indicates the delta of every fade-iteration (1.0  -  0.00000000...1).
 *                The effective range is approximately (0.1  -  0.0001).
 *                A smaller delta means a slower fade.
 *                A positive delta in combination with start<end fades the image in.
 *                A negative delta in combination with start>end fades the image out.
 * 
 * #################################################################################
 */

this.BgImgFader = function() {
  var styleRules;
  
//GET/SET-FUNCTIONS=================================================================
//GET SELECTORS---------------------------------------------------------------------
  function getArray(str) {
    /* This function is invoked by this.prepare() and this.fade().
     * This function converts the specified string of selectors to an array, and returns that.
     */
    //strip trailing comma's
    if (str.indexOf(',')==0) {str.substring(1);} //strip first comma
    if (str.lastIndexOf(',')==str.length-1) {str.substring(0,str.length-1);} //strip last comma
    //store selectors in array
    if (str.indexOf(',')==-1) {var selectors = [str];}
    else {var selectors = str.split(',');}
    //trim trailing spaces
    for (var i=0; i<selectors.length; i++) {
      selectors[i] = selectors[i].trim();
    }
    return selectors;
  }
//GET STYLE SHEET-------------------------------------------------------------------
  function getStyleSheet(style) {
    /* This function is invoked by this.prepare() and this.fade().
     * This function returns a reference to the specified style sheet, 
     * based on either a number or a filename of the sheet.
     * A number is for internal sheets, where the number stands 
     * for its location in the HTML (e.g. the first '<style></style>').
     * A filename is for external sheets (e.g. 'style.css').
     * See the instructions in the header of this file for details.
     */
    if (typeof style === 'number') {
      return document.styleSheets[style];
    } else {
      //find style sheet
      for (var i=0; i<document.styleSheets.length; i++) {
        var file = document.styleSheets[i].href;
        file = file.substring(file.lastIndexOf('/')+1);
        if (file.toLowerCase() == style.toLowerCase()) {
          return document.styleSheets[i];
        }
      }
    }
  }
//SET STYLE RULE--------------------------------------------------------------------
  function addStyleRule(sheet, selector, declarations) {
    /* This function is invoked by this.prepare().
     * This function dynamically adds the specified rule to the specified style sheet.
     */
    if (sheet.addRule) {sheet.addRule(selector,declarations);} //IE...
    else {sheet.insertRule(selector+'{'+declarations+'}');} //NON-IE...
  }
  
//PREPARE===========================================================================
  this.prepare = function(style, selectors) {
    /* This function is invoked by an external function, outside of the library.
     * This function is an interface for external scripts to access this library.
     * The function prepares the elements specified by the selectors, by adding certain style rules 
     * to them that are necessary for this library to successfully manipulate the background-image.
     */
    var selectors = getArray(selectors);
    var styleSheet = getStyleSheet(style);
    for (var i=0; i<selectors.length; i++) {
      addStyleRule(styleSheet,selectors[i],'position:relative;');
      addStyleRule(styleSheet,selectors[i]+'::after','position:absolute; top:0;right:0;bottom:0;left:0; opacity:0.0; content:"";');
    }
  };
  
//FADE BACKGROUND IMAGE=============================================================
//INIT------------------------------------------------------------------------------
  this.fade = function(style, selectors, global, opacity, endOpacity, delta) {
    /* This function is invoked by an external function, outside of the library.
     * This function is an interface for external scripts to access this library.
     * The function initiates the fading process. It first stores the appropriate 
     * set of style rules into the style rules variable, and then invokes 
     * fadeOpacity() to start the fading.
     */
    var selectors = getArray(selectors);
    var styleSheet = getStyleSheet(style);
    styleRules = styleSheet.rules ? styleSheet.rules : styleSheet.cssRules; //IE uses 'rules', NON-IE use 'cssRules'
    fadeOpacity(selectors,global,opacity,endOpacity,delta,[]);
  };
  
//FADE------------------------------------------------------------------------------
  function fadeOpacity(selectors, global, opacity, endOpacity, delta, rules) {
    /* This function is invoked by fade().
     * This function fades the background-image of the specified elements, by 
     * adding the delta to the current opacity, and then setting the opacity 
     * of all specified elements to that new value.
     */
    opacity += delta;
    if (rules.length == 0) {
      //find the css-rules that match the specified selector(s)
      for (var i=0; i<selectors.length; i++) {
        for (var j=0; j<styleRules.length; j++) {
          if (global && styleRules[j].selectorText.toLowerCase().indexOf(selectors[i].toLowerCase()+'::after')!=-1) {
            rules.push(styleRules[j]);
          } else if (styleRules[j].selectorText.toLowerCase() == selectors[i].toLowerCase()+'::after') {
            rules.push(styleRules[j]); break;
          }
        }
      }
    }
    //set the opacity of the background-image for every matched rule
    for (var i=0; i<rules.length; i++) {
      rules[i].style.opacity = opacity;
    }
    //check if the end-opacity is reached
    if (opacity < endOpacity) {
      setTimeout(function(){fadeOpacity(selectors,global,opacity,endOpacity,delta,rules);},0); //invoke itself again
    } else {
      //manually set the opacity to the end-opacity (otherwise it'll be off by a fraction)
      for (var i=0; i<rules.length; i++) {
        rules[i].style.opacity = endOpacity;
      }
      rules.length = 0;
    }
  }
};
source code of library, not a working code snippet!
(source code of library, not a working code snippet)

Interesting fact: This does not work on jsfiddle.net, because their own style sheets get mixed in with the one from the fiddle, making it impossible to determine in which sheet (based on number) the required CSS-rules reside.

Solution 2

Not clear if you solved that already: For regular <img> images putting another <img> above it (display: inline-block; visibility: hidden;) and listening to the onload of the high-res image will work:

$("high-res").load(function() {
    $(this).css({visibility: "hidden", opacity: 0}).fadeIn("slow");
}

EDIT: The other way around (putting the high-res behind the low-res and then fading out the low-res) also works. But you wont get around overlaying stuff.

Fading in CSS background-images is impossible. They have no opacity value. The only thing you can do is put the contents in a <div> above an <img> and fading that in the same way.

Share:
10,959
Admin
Author by

Admin

Updated on June 14, 2022

Comments

  • Admin
    Admin almost 2 years

    The main solution out there is:

    "Just throw a loading screen up until the page is loaded".

    But my goal is to build pages that present the basics very quickly, without a loading screen, and then transition in the images and fancy features when they're ready. So I'll wait till it's loaded, and then fade it in. Or I'll load in a very low res version and then fade in the high res when it's ready.

    The one aspect of this practice that I have not yet figured out is how to do it with background images.

    How can I achieve the affect of a smooth fade in with background images?

    I'm willing to use:

    • Javascript
    • JQuery
    • Any modern JQuery library
    • CSS tricks / "hacks"

    But I want to avoid:

    • Using an overlay element and fading that in on top.
  • Zach Saucier
    Zach Saucier almost 10 years
    I removed some unnecessary parts from your answer. If you have any questions or comments I can address them in a chatroom
  • Admin
    Admin almost 10 years
    Without having gone through your mini library for it, I'd like to see a note of what the purpose behind this library is, e.g. what advantages it offers. Otherwise, the solution I figured out is just a few lines of JS as well and requires no lib.
  • myfunkyside
    myfunkyside almost 10 years
    you've have a solution:O must have missed that, last thing I saw was a transitioning image you called "pretty trippy".. why is there still no answer marked as correct than?
  • Admin
    Admin almost 10 years
    well, have a look at the two limitations to my solution. I'd prefer the select the best one, hence me asking whether yours addresses either of the issues? (I posted what I learned as an answer, that what I mean by "my solution", which is thanks to a comment from @dandavis
  • myfunkyside
    myfunkyside almost 10 years
    No, my solution does not have those limitation, as you can see in the post I have actually set the background-size right there. And since there's no need for a filler image, your second problem doesn't even come into play. (Although if I add background-repeat: no-repeat; to your solution, it seems I can manipulate the size just fine.. then again, I don't know what exactly you want to do with the image and if no-repeat allows that)