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>