Off-canvas img’s not displaying when they entire the viewport with lazy loading

I’m having a few issues with images displaying that begin life off the screen but scroll into view automatically with looping carousels. The images use loading="lazy" and a small bit of Javascript.

I’ve tried this approach with two different versions of the carousel to be sure, both included in the example. A CSS only version and another using Keen Slider.

The Known Issues

  • Keen Slider Version works the best but the images outside of the viewport flicker/flash as they enter the viewport and load. Once the images have all been through the screen, it’s very smooth and can be scrolled left/right seamlessly
  • CSS Only Version has the biggest issues. The first two images which are in the viewport on load display fine. However everything that follows has a blank space where the image should be and they never appear.

The odd thing is I use a tiny image as a placeholder that I stretch to the size of the main image. When the main image has loaded, I hide the smaller image – but this image doesn’t even show on the CSS version, even though it doesn’t have loading="lazy set on it. Which is strange?

I tried setting eager to the images and that seemed to fix – but defeats the point of trying to lazy load.

Desired Result

  • Image blocks should display a tiny, stretched image covering the area where the main image will appear.
  • Once the main image is loaded. Hide the tiny image with a CSS animation.
  • The user then sees the fully loaded main/larger image.
  • If it helps with a solution, the loaded class could just be added when an image begins to enter the viewport?

Any help on this is greatly appreciated, thanks!

/* ==========================================================================
   #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) {
    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)
  }
})




/* ==========================================================================
   #CURSOR
   ========================================================================== */

/**
 * Two div's are on the page which are used to create a custom cursor effect.
 * `.cursor` follows the pointer whereas the `.cursor-trail` is an outer circle
 * that follows the cursor with a delay.
 * 
 * The original script was based on this CodePen example:
 * https://codepen.io/ntenebruso/pen/QWLzVjY
 *
 * But there were performance (lagging and jumping) issues cross-browser, mainly
 * in Safari (Mac OS). So it was re-written and this version performed better.
 * Previously `calc` was used for sizing and top/left values for positioning.
 * Using fixed values instead of `calc` (as we know the size of the cursor) and
 * `transform` wasn't as much of a performance hit. If we changed the size of
 *  the cursor in the CSS we'd need to update the values here accordingly.
 */

var cursorTrail = document.querySelector(".cursor-trail");
var a = document.querySelectorAll("a");
var timeout;

window.addEventListener(
  "mousemove",
  function(e) {
    var x = e.clientX;
    var y = e.clientY;

    if (!timeout) {
      timeout = setTimeout(function() {
        timeout = null;
        cursorTrail.style.transform = `translate(${x - 4}px, ${y - 4}px)`;
      }, 24);
    }
  },
  false
);

/**
 * Add/remove set classes on hover.
 * 
 * 1. This used to start with `a.forEach((item) => {` but changed to `let` so
 *    that an additional (non-anchor) item could be targeted. `#hello` is for
 *    the image on the 404 page.
 */

// a.forEach((item) => {
let links = document.querySelectorAll('a'); /* [1] */
links.forEach((item) => {
  /* [1] */
  item.addEventListener("mouseover", () => {
    cursorTrail.classList.add("cursor-trail--hover");
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove("cursor-trail--hover");
  });
});

/**
 * Add/remove classes on click (anywhere).
 */

document.addEventListener("mousedown", function() {
  cursorTrail.classList.add("cursor-trail--click");
});

document.addEventListener("mouseup", function() {
  cursorTrail.classList.remove("cursor-trail--click");
});

/**
 * Add custom classes on hover if the cursor needs to be manipulated in a
 * unique way. If an element has a `data-interaction=""` value set. This will
 * be added as a class to the cursor on hover. For example, this is used to
 * style the prev/next arrows on the carousel.
 *
 * This could be set using a specific class but I've just left it targeting all
 * `a` elements for now. Which will add a class of `undefined` if no dataset is
 * specified.
 */

a.forEach((item) => {
  const interaction = item.dataset.interaction;

  item.addEventListener("mouseover", () => {
    cursorTrail.classList.add(interaction);
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove(interaction);
  });
});

/**
 * Text Label
 */

let hasLabel = document.querySelectorAll('.has-label');
var cursorText = document.querySelector('.cursor__label-text');

hasLabel.forEach((item) => {
  const label = item.dataset.label;

  item.addEventListener("mouseover", () => {
    cursorText.textContent = label;
    cursorTrail.classList.add("cursor-trail--label");
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove("cursor-trail--label");
  });
});
/* ==========================================================================
   #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%;
}



/* ==========================================================================
   #CURSOR
   ========================================================================== */

/**
 * Hide the cursor if the `hover` event doesn't exist so the custom cursor isn't
 * displayed on touch devices.
 */

@media (hover: none) {
  .cursor-trail {
    display: none;
  }
}

/**
 * Core `.cursor-trail` styling which is a larger cricle that follows `.cursor`
 * around the screen but with a smooth delay/lag.
 *
 * 1. Fade-in the object only when `body` is hovered over. Otherwise on page
 *    load the cursor is stuck in the top/left of the browser until you move the
 *    mouse, which looks a bit crap. At least this is a bit more graceful.
 * 2. Add a lot transitions for various click/hover states which adjust the size
 *    and colour of the element. `transform` is the one for the smooth lag when
 *    moving the cursor.
 */

.cursor-trail {
  background: white;
  box-sizing: border-box;
  height: 8px;
  margin: 0;
  opacity: 0;
  /* [1] */
  pointer-events: none;
  position: fixed;
  transform-origin: center center;
  transition: height 0.04s ease-out, margin 0.04s ease-out, opacity 0.16s 0.16s, transform 0.32s cubic-bezier(0, 0.48, 0.64, 1), width 0.08s ease-out;
  /* [2] */
  width: 8px;
  mix-blend-mode: difference;
  z-index: 1000;
}

body:hover .cursor-trail {
  opacity: 1;
}

/**
 * A class of `.cursor-trail--hover` is added when the user hovers over a link.
 * When this occurs we shrink the trailing div. The `margin` keeps the div
 * centred with `.cursor` when triggered. Previously a `transform` was used to
 * centre the div - but pixel values just meant less was being calculated at
 * the same time and led to better/smoother performance.
 */

.cursor-trail--hover {
  height: 0;
  margin: 4px 0 0 4px;
  width: 0;
}

/**
 * If an element on the page has a `data-label` set, Javascript gets the value
 * and displays the text within `.cursor__label-text` to a label can be
 * displayed alongside the cursor.
 *
 * 1. The parent of the label which has `overflow: hidden` set, allows the text
 *    to be animated vertically in/out of view on hover.
 */

.cursor__label {
  color: white;
  overflow: hidden;
  /* [1] */
  position: absolute;
  top: -8px;
  left: 16px;
}

/**
 * 1. The label begins out of view, pushed beyond the bottom edge of the parent.
 * 2. When an element with a label is hovered over, a class and animation is
 *    added to the main `.cursor-trail` element. This allows us to animate the
 *    text into view. Only running the animation when the class is added, not
 *    when it is removed...
 * 3. The `transition` only runs when `.cursor-trail--label` is removed from the
 *    parent. As the `animation` only runs when the class is added, it allows us
 *    to `transition` the text out the top edge of it's container without the
 *    animation interfering with it.
 */

.cursor__label-text {
  display: block;
  opacity: 0;
  transform: translateY(-100%);
  /* [1] */
  transition: opacity 0.16s ease-out, transform 0.16s ease-out;
  /* [3] */
  white-space: nowrap;
}

.cursor-trail--label .cursor__label-text {
  /* [2] */
  animation: cursor-label 0.16s ease-out;
  opacity: 1;
  transform: translateY(0);
}

/**
 * Animation which moves the label into view from the bottom of it's parent.
 */

@keyframes cursor-label {
  0% {
    opacity: 0;
    transform: translateY(100%);
  }

  100% {
    opacity: 1;
    transform: translateY(0);
  }
}




/* ==========================================================================
   #MARQUEE
   ========================================================================== */

/**
 * Auto-scrolling gallery displaying images of projects.
 *
 * 1. Animation settings.
 */

:root {
  /* [1] */
  --play: running;
  --direction: normal;
  --duration: 32s;
  --delay: 0s;
  --iteration-count: infinite;
}

/**
 * As we're using a CSS auto-scrolling carousel, we need a duplicate `.marquee`
 * for a seamless, continuous loop. So we need a wrapping div to contain them
 * both.
 *
 * 1. Ensure both `.marquee` elements display in a row.
 * 2. Prevent any horizontal scrolling of the page.
 */

.marquee-wrap {
  display: flex;
  /* [1] */
  flex-direction: row;
  /* [1] */
  margin-bottom: 64px;
  overflow: hidden;
  /* [2] */
}

/**
 * The two `.marquee` elements are styled and behave exactly the same. The only
 * difference is the 2nd one has `aria-hidden="true"` set for SEO.
 *
 * 1. Prevent the content from shrinking.
 */

.marquee {
  display: flex;
  /* [1] */
  flex-shrink: 0;
  /* [1] */
  animation: marquee-scroll var(--duration) linear var(--delay) var(--iteration-count);
  animation-play-state: var(--play);
  animation-delay: var(--delay);
  animation-direction: var(--direction);
  min-width: 100%;
  /* [1] */
}

/**
 * Set the height/width of each item and prevent it from shrinking.
 */

.marquee__item {
  flex-shrink: 0;
  margin-right: 8px;
  width: 320px;
}

/**
 * Animate the content from left to right.
 */

@keyframes marquee-scroll {
  0% {
    transform: translateX(0%);
  }

  100% {
    transform: translateX(-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] */
}




/* ==========================================================================
   #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>
    <div class="cursor-trail">
      <div class="cursor__label"><span class="cursor__label-text"></span></div>
    </div>

    <h2>1. Keen Slider Version</h2>

    <!-- 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>
    </div>
    <!-- End Keen Slider -->

    <h2>2. CSS Marquee Version</h2>

    <!-- Gallery -->
    <div class="marquee-wrap">

      <div class="marquee">
        <figure data-label="Hover Label 1" class="marquee__item 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>
        <figure data-label="Hover Label 2" class="marquee__item 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>
        <figure data-label="Hover Label 3" class="marquee__item 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>
        <figure data-label="Hover Label 4" class="marquee__item 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>
        <figure data-label="Hover Label 5" class="marquee__item 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>
        <figure data-label="Hover Label 6" class="marquee__item 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>
        <figure data-label="Hover Label 1" class="marquee__item 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>
        <figure data-label="Hover Label 8" class="marquee__item 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 class="marquee" aria-hidden="true">
        <figure data-label="Hover Label 1" class="marquee__item 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>Clone Slide 1</figcaption>
        </figure>
        <figure data-label="Hover Label 2" class="marquee__item 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>Clone Slide 2</figcaption>
        </figure>
        <figure data-label="Hover Label 3" class="marquee__item 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>Clone Slide 3</figcaption>
        </figure>
        <figure data-label="Hover Label 4" class="marquee__item 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>Clone Slide 4</figcaption>
        </figure>
        <figure data-label="Hover Label 5" class="marquee__item 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>Clone Slide 5</figcaption>
        </figure>
        <figure data-label="Hover Label 6" class="marquee__item 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>Clone Slide 6</figcaption>
        </figure>
        <figure data-label="Hover Label 1" class="marquee__item 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>Clone Slide 7</figcaption>
        </figure>
        <figure data-label="Hover Label 8" class="marquee__item 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>Clone Slide 8</figcaption>
        </figure>
      </div>

    </div>
    <!-- End Gallery -->

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

</html>