
Adding awesome visual polish on our websites doesn’t have to be complicated. We can make our own smooth and professional looking fade effects on images by using just a few simple lines of JavaScript. Fades are awesome because they give us a smooth user experience when images need to load.
We’ll look at some JavaScript fade techniques that we can try out for ourselves and see how Cloudinary’s progressive loading features does all the heavy lifting for us. We’ll build basic opacity transitions with image reveals that trigger by scrolling down, and they look really good. Last but not least, we’ll also add automatic placeholders to image fades.
Key Takeaways:
- Fades let us smooth out visual changes when images load or appear
- CSS transitions with JavaScript give us fade effects that perform really well
- Fades that trigger while scrolling can improve a user’s experience on pages with lots of content
In this article:
- What Is a Fade? When to Use It
- Quick Start for Fading Images with JavaScript
- Hover, Click, and Slideshow Fades
- Managing Timing Functions for Smoother Fades
- Use Cloudinary for Progressive-Loading Fade Effects
What Is a Fade? When to Use It
We can think about a fade as a change that happens gradually in an image’s opacity, or how solid it looks. Transparent images gradually go from being transparent to visible for fade-ins, and fade-outs are the opposite. We use fades because they are useful, not merely for their visual appeal.
The first reason is that fades prevent that unsettling “pop-in” effect that happens when images load on slow connections, where they just appear out of nowhere. Instead of an empty space suddenly filling with content, users get to see a smooth transition that feels like it was done on purpose. This is so important for hero images and above-the-fold content where a site visitor’s first impressions can make or break their user experience.
Fades also work brilliantly for lazy-loaded images. As users scroll down our page, images start to appear gradually instead of materializing out of nowhere. This gives us a better browsing experience and keeps users engaged with our content.
Quick Start for Fading Images with JavaScript
Here’s some basic code we can try for ourselves. We’ll start with the simplest approach and build up to some more advanced techniques.
Plain JavaScript: Fade In/Out and on Scroll
The foundation of any JavaScript fade effect is CSS opacity mixed in with transitions. Here’s our basic HTML structure:
<div class="image-container">
<img src="your-image.jpg" alt="Demo image" class="fade-image" id="demoImage">
</div>
<button id="fadeToggle">Toggle Fade</button>
Now let’s add the CSS that handles the animation:
.fade-image {
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.fade-image.visible {
opacity: 1;
}
The CSS does most of the work here. We set our starting opacity to 0 and then add a half-second transition. The .visible class just changes the opacity to 1, and CSS handles the smooth animation between the two different states.
Here’s our JavaScript that controls the fade:
document.addEventListener('DOMContentLoaded', function() {
const image = document.getElementById('demoImage');
const toggleBtn = document.getElementById('fadeToggle');
// Fade in when our image loads
image.addEventListener('load', function() {
image.classList.add('visible');
});
// Toggle fade on our button click
toggleBtn.addEventListener('click', function() {
image.classList.toggle('visible');
});
});
This code listens for the image’s load event and adds our .visible class to it when the image is ready. The toggle button lets us manually control the fade in the demo. See how we’re not manually animating anything? We just toggle a class and let CSS handle the transition for us which is pretty neat!
For fades that kick in when we scroll, we can use the Intersection Observer API. There are a few reasons for this, but the main one is that it’s more efficient than listening to scroll events directly:
document.addEventListener('DOMContentLoaded', function() {
const fadeImages = document.querySelectorAll('.fade-on-scroll');
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.2,
rootMargin: '0px 0px -50px 0px'
});
fadeImages.forEach(function(img) {
observer.observe(img);
});
});
The Intersection Observer watches our images and triggers the fade when they’re 20% visible in the viewport, then we use rootMargin to trigger slightly before the image fully comes into view, which makes it feel more natural to users. When an image fades in, we stop watching it to save resources, keeping things efficient.
Hover, Click, and Slideshow Fades
Interactive fades respond to user actions like a mouse click or hovering over it. Let’s build a hover effect that reveals more information about an image:
<div class="hover-container">
<img src="product.jpg" alt="Product" class="product-image">
<div class="overlay">
<h3>Product Name</h3>
<p>$29.99</p>
</div>
</div>
.hover-container {
position: relative;
overflow: hidden;
}
.overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 20px;
opacity: 0;
transition: opacity 0.3s ease;
}
.hover-container:hover .overlay {
opacity: 1;
}
This creates a product card, and hovering over it shows us pricing information. The CSS handles everything so there’s no JavaScript for simple hover states.
For fades that trigger on a user click we can add JavaScript:
document.addEventListener('DOMContentLoaded', function() {
const containers = document.querySelectorAll('.click-reveal');
containers.forEach(function(container) {
container.addEventListener('click', function() {
const overlay = this.querySelector('.overlay');
overlay.classList.toggle('visible');
});
});
});
Now let’s build an image slideshow with crossfade transitions:
document.addEventListener('DOMContentLoaded', function() {
const images = document.querySelectorAll('.slideshow-image');
let currentIndex = 0;
function crossfade(nextIndex) {
// Fade out our current image
images[currentIndex].classList.remove('active');
// Fade in our next image
images[nextIndex].classList.add('active');
currentIndex = nextIndex;
}
// Auto-advance every 4 seconds
setInterval(function() {
const nextIndex = (currentIndex + 1) % images.length;
crossfade(nextIndex);
}, 4000);
});
The slideshow uses absolute positioning so images stack up on top of each other. Only the .active image has full opacity so the others stay transparent.
Managing Timing Functions for Smoother Fades
The timing function decides how our fade acts over time. Linear fades can look a little mechanical sometimes, and eased fades feel more natural. Here are some options for us to try:
/* Different timing functions */
.fade-linear {
transition: opacity 0.5s linear;
}
.fade-ease {
transition: opacity 0.5s ease;
}
.fade-ease-in {
transition: opacity 0.5s ease-in;
}
.fade-ease-out {
transition: opacity 0.5s ease-out;
}
.fade-ease-in-out {
transition: opacity 0.5s ease-in-out;
}
/* Custom cubic-bezier for more control */
.fade-custom {
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
For fade-ins, ease-out works best for us because the animation starts quickly (roughly when users notice it) and slows down as it finishes. For fade-outs, ease-in generally feels more natural. The ease-in-out option works well for bidirectional fades like we saw in our toggle button example.
The duration also matters. Quick interactions like hovers prefer 200 to 300ms transitions, and page elements loading on scroll feel better with 400 to 600ms. Hero images and dramatic reveals can go up to 1 second, but anything longer starts feeling a bit sluggish.
Use Cloudinary for Progressive-Loading Fade Effects
Building fade effects manually works great for interactive elements, but image loading is where things get a little more challenging. We need to handle network latency and different file sizes while we also deal with different connection speeds from users. Again, Cloudinary steps in and handles the complicated parts for us.
Cloudinary’s progressive loading capabilities generate low-quality placeholders automatically for us, and they handle the transition into high-quality images. We end up with professional fade effects without us needing to write complex loading logic ourselves.
Use Low-Quality Placeholders (LQIP) from Cloudinary
Low-quality image placeholders (LQIPs) are tiny versions of our images that load almost instantly, and Cloudinary generates these automatically with a simple URL transformation.
Here’s how we generate an LQIP from our Cloudinary image:
<!-- Original image --> https://res.cloudinary.com/our-cloud-name/image/upload/sample.jpg <!-- LQIP version (blurred, low quality) --> https://res.cloudinary.com/our-cloud-name/image/upload/e_blur:1000,q_auto:low,w_50/sample.jpg
The transformation e_blur:1000,q_auto:low,w_50 creates a very blurred 50-pixel-wide image that usually weighs in at under 2KB and it loads almost instantly on most connections.
Cloudinary offers a few different placeholder styles too:
<!-- Blurred placeholder --> e_blur:1000,q_auto:low,w_50 <!-- Pixelated placeholder --> e_pixelate:20,q_auto:low,w_50 <!-- Predominant color (solid color based on image) --> e_blur:2000,q_1,w_1
The predominant color option gives us a single pixel image that displays the dominant color of the image, and is perfect for seamless content loading without layout shifts.
Fade from a Tiny Placeholder to a Sharp Image Automatically
Now we can combine our JavaScript fade techniques with Cloudinary’s LQIPs. Here’s how:
<div class="progressive-image-container">
<img
class="placeholder"
src="https://res.cloudinary.com/your-cloud-name/image/upload/e_blur:1000,q_auto:low,w_50/sample.jpg"
alt="Loading..."
>
<img
class="full-image"
data-src="https://res.cloudinary.com/your-cloud-name/image/upload/q_auto,f_auto/sample.jpg"
alt="Sample image"
>
</div>
<style>
.progressive-image-container {
position: relative;
overflow: hidden;
}
.progressive-image-container .placeholder {
width: 100%;
height: auto;
filter: blur(20px);
transform: scale(1.1);
transition: opacity 0.4s ease-out;
}
.progressive-image-container .full-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
opacity: 0;
transition: opacity 0.4s ease-in;
}
.progressive-image-container .full-image.loaded {
opacity: 1;
}
.progressive-image-container .placeholder.hidden {
opacity: 0;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const containers = document.querySelectorAll('.progressive-image-container');
containers.forEach(function(container) {
const placeholder = container.querySelector('.placeholder');
const fullImage = container.querySelector('.full-image');
// Load the full image
fullImage.src = fullImage.dataset.src;
fullImage.addEventListener('load', function() {
// Crossfade: show the full image, and hide the placeholder
fullImage.classList.add('loaded');
placeholder.classList.add('hidden');
});
});
});
<script/>
This gives us a smooth crossfade from a blurry placeholder to the high quality final image. The placeholder loads instantly and fills the space while Cloudinary gives us our optimized full res image. The main benefit is that users see something right away instead of an empty container.
We can make this even better by adding lazy loading:
document.addEventListener('DOMContentLoaded', function() {
const containers = document.querySelectorAll('.progressive-image-container');
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const container = entry.target;
const fullImage = container.querySelector('.full-image');
const placeholder = container.querySelector('.placeholder');
fullImage.src = fullImage.dataset.src;
fullImage.addEventListener('load', function() {
fullImage.classList.add('loaded');
placeholder.classList.add('hidden');
});
observer.unobserve(container);
}
});
}, { rootMargin: '50px' });
containers.forEach(function(container) {
observer.observe(container);
});
});
Now our images only start loading when they’re about to enter the viewport. The LQIP makes sure that we never have empty space, and Cloudinary’s automatic format and quality optimization allows the full image to arrive as fast as possible.
Wrapping Up
We’ve gone over some of the basic techniques used for making smooth fade effects using JavaScript. We’ve covered simple opacity toggles to scroll based reveals, and progressive image loading. These different patterns give us the tools that we need to build a better user experience that also looks great.
The main thing that we learned is that CSS handles the animation and JavaScript controls when transitions happen. Separating these different elements keeps our code clean and lets our animations run well at the same time. For image loading, Cloudinary’s LQIP feature deals with the headache of progressive loading, and it lets us think about the user experience instead of network optimization.
Sign up for a free Cloudinary account and start using your own professional fade effects today.
Frequently Asked Questions
How do I make images fade in smoothly when they load?
We use CSS transitions with JavaScript’s load events. We set the image’s initial opacity to 0 with a CSS transition, and then add a class that changes opacity to 1 once the image finishes loading. The browser handles the smooth animation between states, and the effect triggers at exactly the right moment when the image data is ready to be shown on the screen.
What’s the best way to fade images on scroll?
The Intersection Observer API is the most efficient way to deal with scroll fades. It watches elements and fires callbacks when they enter or leave the viewport without the performance hit of using scroll event listeners. We can configure the threshold and margin to trigger fades at exactly the right moment when users scroll through our content.
Can I create fade effects without writing JavaScript?
Yes you can. With hover based fades we can use pure CSS with the :hover pseudo class and transitions. But for load triggered fades and scroll based reveals we’ll need to stick JavaScript to see when images finish loading or when elements enter into the viewport. Cloudinary’s SDKs for frameworks like React and Angular have built in placeholder and lazy loading plugins that will handle this automatically for us.