skip to Main Content

I have small bit Javascript that detects if an image has been loaded by the browser, then adds a class of .loaded to its parent.

The img elements all have loading="lazy" declared on them.

With CSS, when the class is added, I transition/animate out a placeholder image which over over the top of the ‘main’ inage. The placeholder image is a tiny 16×24 image that is stretched, loaded using eager and pixelated using image-rendering: pixelated;. Which gives quite a nice effect – that might not come across in the example due to the slow loading of both hosting images.

The Problem

As far as I can tell this all works …but the ‘problem’ (in this case) is all the images load pretty instantly. I was wondering if it’s possible to defer the loading of images outside the viewport and only have them load once a % of the image has entered the viewport. So the class is added later and you see this visual effect.

The Why

The reason for this is, as you can see I’m using a JS carousel. On iOS devices the images when they enter the viewport the first time jump/flicker into place. However if I remove the images (so they don’t load in), the pixel images display perfectly as you scroll.

So I’m thinking if I can defer the loading of the main/larger image, the stretched pixel image, transitioning into the main image will just look a bit better than there being white space for a second.

That issue is strange as technically the image should’ve loaded in full but this seems like a work around for me – hopefully.

Thanks in advance and hope someone can give some guidance on this!

/* ==========================================================================
   #LAZY LOAD IMAGES
   ========================================================================== */

/**
 * Class to animate/transition an image into view once it has been loaded.
 */

const pixelImage = document.querySelectorAll(".pixel-load")

pixelImage.forEach(div => {
  const img = div.querySelector("img")

  function loaded() {
    div.classList.add("loaded")
  }

  if (img.complete) {
    loaded()
  } else {
    img.addEventListener("load", loaded)
  }
})




/* ==========================================================================
   #KEEN SLIDER
   ========================================================================== */

/**
 * Using Keen-Slider for the infinite looping carousel, which I originally did
 * in pure CSS - but I wanted to make this draggable by the user so made sense
 * to use a 3rd party plug-in to do the heavy lifting.
 */

var animation = {
  duration: 32000,
  easing: (t) => t
}
new KeenSlider("#gallery-slider", {
  dragSpeed: 1,
  loop: true,
  mode: "free",
  slides: {
    perView: 1.5,
    renderMode: "performance",
    spacing: 8
  },
  breakpoints: {
    '(min-width: 768px)': {
      slides: {
        perView: 3,
        spacing: 8
      }
    },
    '(min-width: 1024px)': {
      slides: {
        perView: 4,
        spacing: 8
      }
    }
  },
  created(s) {
    document.querySelector("#gallery-slider").classList.add("loaded");
    s.moveToIdx(5, true, animation);
  },
  updated(s) {
    s.moveToIdx(s.track.details.abs + 5, true, animation);
  },
  animationEnded(s) {
    s.moveToIdx(s.track.details.abs + 5, true, animation);
  }
})
/* ==========================================================================
   #BASE
   ========================================================================== */

html {
  font-size: 62.5%;
  margin: 0;
  padding: 0;
}

body {
  font-size: 12px;
  font-family: "Arial", sans-serif;
  margin: 0;
  padding: 64px 0 0;
  text-transform: uppercase;
}

h2 {
  font-size: 12px;
  font-weight: 400;
  margin: 0 16px 16px;
  padding: 0;
}

figure {
  margin: 0;
  padding: 0;
}

img {
  height: auto;
  width: 100%;
  max-width: 100%;
}


/* ==========================================================================
   #KEEN SLIDER
   ========================================================================== */


/**
 * 1. Removed `overflow: hidden` so I could align the slider with the main grid
 *    but still have it bleed off the edges of the page. To avoid a horizontal
 *    scroll on the site, I've added `overflow: hidden` to a parent div.
 */

.keen-slider:not([data-keen-slider-disabled]) {
  display: flex;
  align-content: flex-start;
  overflow: hidden;
  position: relative;
  touch-action: pan-y;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  width: 100%;
  -webkit-tap-highlight-color: transparent;
}

.keen-slider:not([data-keen-slider-disabled]) .keen-slider__slide {
  min-height: 100%;
  overflow: hidden;
  position: relative;
  width: 100%;
}

.keen-slider:not([data-keen-slider-disabled])[data-keen-slider-v] {
  flex-wrap: wrap;
}


/* ==========================================================================
   #GALLERY
   ========================================================================== */


/**
 * My overrides for the Keen Slider gallery.
 *
 * 1. Remove `overflow: hidden` from the slider and add it to the parent. This
 *    allows the slider to align with the grid but also bleed off the edges of
 *    the page.
 * 2. Align container with the global grid.
 */

.gallery {
  margin-bottom: 64px;
  overflow: hidden;
  /* [1] */
  padding: 0 16px;
  /* [2] */
}

.gallery .keen-slider {
  overflow: visible;
  /* [1] */
}


/**
 * As the widths for each slide are set in Javascript. We add widths to slides
 * before `.keen-slider` has loaded to keep the layout consistent and help with
 * the Cumulative Layout Shift (CLS) and performance.
 */

.keen-slider:not(.loaded) .keen-slider__slide {
  width: calc((100vw / 1.5) - 24px);
}

@media(min-width: 48em) {
  .keen-slider:not(.loaded) .keen-slider__slide {
    width: calc((100vw - 48px) / 3);
  }
}

@media(min-width: 64rem) {
  .keen-slider:not(.loaded) .keen-slider__slide {
    width: calc((100vw - 56px) / 4);
  }
}


/* ==========================================================================
   #PIXEL LOAD
   ========================================================================== */


/**
 * Add a pixelated effect to images while the load.
 */

.pixel-load {
  overflow: hidden;
  position: relative;
}

.pixel-load__preload img {
  image-rendering: pixelated;
  position: absolute;
  inset: 0;
  opacity: 1;
  pointer-events: none;
}

.loaded .pixel-load__preload img {
  animation: loaded 0.32s 0.32s steps(1, end) both;
}

@keyframes loaded {
  0% {
    scale: 1.1;
  }
  64% {
    scale: 1.04;
  }
  75% {
    opacity: 0.8;
    scale: 1.02;
  }
  100% {
    opacity: 0;
    z-index: 1;
  }
}
<!-- Keen Slider -->
<div class="gallery">
  <div id="gallery-slider" class="keen-slider">
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 1" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 1</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 2" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 2</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 3" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 3</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 4" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 4</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 5" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 5</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 6" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 6</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 7" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 7</figcaption>
      </figure>
    </div>
    <div class="keen-slider__slide">
      <figure data-label="Hover Label 8" class="has-label">
        <div class="pixel-load">
          <div class="pixel-load__preload">
            <img src="https://placebeard.it/18/24" width="18" height="24" loading="eager">
          </div>
          <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
        </div>
        <figcaption>Slide 8</figcaption>
      </figure>
    </div>
  </div>
</div>
<!-- End Keen Slider -->

<script src="https://cdn.jsdelivr.net/npm/[email protected]/keen-slider.min.js"></script>

2

Answers


  1. Chosen as BEST ANSWER

    I've update the previous code so it works with IntersectionObserver.

    I'd appreciate feedback but it looks as if it creates the desired effect/result and animates once the image enters the viewport - and I'm hoping the code checks the image has loaded before adding the class. It seems to.

    It's a bit flakey on resize. So maybe I need this to run again if the viewport is resized so the slides update. Otherwise from mobile screen width to desktop, some of the image stay pixelated when resized to a larger viewport, like the script doesn't pick up the change that they're not visible on resize?

    /* ==========================================================================
       #LAZY LOAD IMAGES
       ========================================================================== */
    
    /**
     * Class to animate/transition an image into view once it has been loaded.
     */
    
    const pixelImage = document.querySelectorAll(".pixel-load")
    
    const observer = new IntersectionObserver(
        entries => {
            entries.forEach(entry => {
                
                const img = document.querySelector(".pixel-load > img")
                            
                function loaded() {
                    entry.target.classList.toggle("loaded", entry.isIntersecting)
                }
                
                if (img.complete) {
                    loaded()
                } else {
                    img.addEventListener("load", loaded)
                }
                
                if (entry.isIntersecting) observer.unobserve(entry.target)
            })
        },
        {
            threshold: 0
        }
    )
    
    pixelImage.forEach(image => {
        observer.observe(image)
    })
    
    
    
    
    /* ==========================================================================
       #KEEN SLIDER
       ========================================================================== */
    
    /**
     * Using Keen-Slider for the infinite looping carousel, which I originally did
     * in pure CSS - but I wanted to make this draggable by the user so made sense
     * to use a 3rd party plug-in to do the heavy lifting.
     */
    
    var animation = {
      duration: 32000,
      easing: (t) => t
    }
    new KeenSlider("#gallery-slider", {
      dragSpeed: 1,
      loop: true,
      mode: "free",
      slides: {
        perView: 1.5,
        renderMode: "performance",
        spacing: 8
      },
      breakpoints: {
        '(min-width: 768px)': {
          slides: {
            perView: 3,
            spacing: 8
          }
        },
        '(min-width: 1024px)': {
          slides: {
            perView: 4,
            spacing: 8
          }
        }
      },
      created(s) {
        s.moveToIdx(5, true, animation)
      },
      updated(s) {
        s.moveToIdx(s.track.details.abs + 5, true, animation)
      },
      animationEnded(s) {
        s.moveToIdx(s.track.details.abs + 5, true, animation)
      }
    })
    /* ==========================================================================
       #BASE
       ========================================================================== */
    
    html {
      font-size: 62.5%;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-size: 12px;
      font-family: "Arial", sans-serif;
      margin: 0;
      padding: 128px 0 0;
      text-transform: uppercase;
    }
    
    h2 {
      font-size: 12px;
      font-weight: 400;
      margin: 0 16px 16px;
      padding: 0;
    }
    
    p {
      margin: 0 16px 128px;
    }
    
    figure {
      margin: 0;
      padding: 0;
    }
    
    img {
      height: auto;
      width: 100%;
      max-width: 100%;
    }
    
    
    /* ==========================================================================
       #KEEN SLIDER
       ========================================================================== */
    
    .keen-slider:not([data-keen-slider-disabled]) {
      display: flex;
      align-content: flex-start;
      overflow: hidden;
      position: relative;
      touch-action: pan-y;
      -webkit-user-select: none;
      -moz-user-select: none;
      user-select: none;
      width: 100%;
      -webkit-tap-highlight-color: transparent;
    }
    
    .keen-slider:not([data-keen-slider-disabled]) .keen-slider__slide {
      min-height: 100%;
      overflow: hidden;
      position: relative;
      width: 100%;
    }
    
    .keen-slider:not([data-keen-slider-disabled])[data-keen-slider-v] {
      flex-wrap: wrap;
    }
    
    
    
    
    /* ==========================================================================
       #GALLERY
       ========================================================================== */
       
    /**
     * My overrides for the Keen Slider gallery.
     *
     * 1. Remove `overflow: hidden` from the slider and add it to the parent. This
     *    allows the slider to align with the grid but also bleed off the edges of
     *    the page.
     * 2. Align container with the global grid.
     */
    
    .gallery {
        margin-bottom: 128px;
        overflow: hidden; /* [1] */
        padding: 0 16px; /* [2] */
    }
    
    .gallery .keen-slider {
      overflow: visible; /* [1] */
    }
    
    /**
     * As the widths for each slide are set in Javascript. We add widths to slides
     * before `.keen-slider` has loaded to keep the layout consistent and help with
     * the Cumulative Layout Shift (CLS) and performance.
     */
    
    #gallery-slider:not(.loaded) .keen-slider__slide {
      width: calc((100vw / 1.5) - 24px);
    }
    
    @media(min-width: 48em) {
      .keen-slider:not(.loaded) .keen-slider__slide {
        width: calc((100vw - 48px) / 3);
      }
    }
    
    @media(min-width: 64rem) {
      .keen-slider:not(.loaded) .keen-slider__slide {
        width: calc((100vw - 56px) / 4);
      }
    }
    
    
    
    
    /* ==========================================================================
       #PIXEL LOAD
       ========================================================================== */
    
    /**
     * Add a pixelated effect to images while the load.
     */
    
    .pixel-load {
      overflow: hidden;
      position: relative;
    }
    
    .pixel-load__preload img {
      image-rendering: pixelated;
      position: absolute;
      inset: 0;
      opacity: 1;
      pointer-events: none;
    }
    
    .loaded .pixel-load__preload img {
      animation: loaded 0.32s 0.32s steps(1, end) both;
    }
    
    @keyframes loaded {
      0% {
        scale: 1.1;
      }
      64% {
        scale: 1.04;
      }
      75% {
        opacity: 0.8;
        scale: 1.02;
      }
      100% {
        opacity: 0;
        z-index: 1;
      }
    }
    <html>
    
    <body>
    
      <h2>Placeholder text to push content down and create vertical scroll</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
        in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat https://stackoverflow.com/questions/ask#non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    
      <!-- Keen Slider -->
      <div class="gallery">
        <div id="gallery-slider" class="keen-slider">
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 1" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 1</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 2" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 2</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 3" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 3</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 4" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 4</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 5" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 5</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 6" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 6</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 7" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 7</figcaption>
            </figure>
          </div>
          <div class="keen-slider__slide">
            <figure data-label="Hover Label 8" class="has-label">
              <div class="pixel-load">
                <div class="pixel-load__preload">
                  <img src="https://placebeard.it/18/24" width="18" height="24">
                </div>
                <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
              </div>
              <figcaption>Slide 8</figcaption>
            </figure>
          </div>
        </div>
        <!-- End Keen Slider -->
    
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/keen-slider.min.js"></script>
    
    </body>
    
    </html>


  2. It’s 2024. You don’t need to fiddle around with placeholder images and all this complexity. Web standards and web browsers now have native support for lazy loading of images and iframes. There are lots of tutorials out there on the web which are now completely out-of-date.

    Just use your original images in your HTML, set loading="lazy", and the browser will take care of the rest. You can confirm in the network tab of your browser inspector that only the images which are in the viewport are loaded initially, then the others will be loaded as you scroll.

    Read more

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search