Skip to content

Instantly share code, notes, and snippets.

@cameronapak
Last active December 16, 2023 05:53
Show Gist options
  • Save cameronapak/30a0c89d255a2bf0f742b4aecda319c5 to your computer and use it in GitHub Desktop.
Save cameronapak/30a0c89d255a2bf0f742b4aecda319c5 to your computer and use it in GitHub Desktop.
A potential way to easily fade in images using a custom Alpine directive. This helps prevent page jumping on image load.
function encodeSvg(svgString) {
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0?permalink_comment_id=4601690#gistcomment-4601690
return svgString.replace(/[<>#%{}"]/g, (x) => '%' + x.charCodeAt(0).toString(16));
}
function createLoadingSvg(loadingColor) {
return `<svg width="1" height="1" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1" fill="${loadingColor}"/></svg>`;
}
function getLoadingBackgroundImage(el, loadingColor) {
const svgString = createLoadingSvg(loadingColor);
const encodedSvg = encodeSvg(svgString);
return 'data:image/svg+xml,' + encodedSvg;
}
function validateImageDimensions(el) {
const imgElHeight = el.clientHeight || el.offsetHeight || el.height;
const imgElWidth = el.clientWidth || el.offsetWidth || el.width;
if (!imgElHeight && !imgElWidth) {
throw new Error('You must provide a height or width value to the image.');
} else {
el.style.height = imgElHeight || 'fit-content';
el.style.width = imgElWidth || 'fit-content';
}
}
function setInitialImageBackground(el, loadingColor, aspectRatio) {
validateImageDimensions(el);
// Sets the loading background image color.
const backgroundUri = getLoadingBackgroundImage(el, loadingColor);
el.src = backgroundUri;
if (aspectRatio) {
el.style.aspectRatio = aspectRatio;
}
}
function setupImageReplacement(el, src, hasInlineObjectFit, Alpine, fadeDuration) {
// Wait for the new image to load
loadImage(el.src, () => {
fadeImageIn(el, src, hasInlineObjectFit, Alpine, fadeDuration);
}, (error) => {
console.error('error!', error);
});
}
function loadImage(src, loadCallback, errorCallback) {
const imgEl = new Image();
imgEl.addEventListener('load', () => {
loadCallback();
imgEl.removeEventListener('load', loadCallback);
});
imgEl.addEventListener('error', (error) => {
errorCallback(error);
imgEl.removeEventListener('error', errorCallback);
});
imgEl.src = src;
}
function fadeImageIn(el, src, hasInlineObjectFit, Alpine, fadeDuration) {
el.style.opacity = '0';
setTimeout(() => {
el.style.transition = `opacity ${fadeDuration} ease-in-out`;
Alpine.nextTick(() => {
el.src = src;
el.style.opacity = '1';
el.style.objectFit = hasInlineObjectFit || '';
});
}, 50);
}
document.addEventListener('alpine:init', () => {
Alpine.directive('fade-in-img', (el, { expression }, { evaluate, Alpine }) => {
const { aspectRatio, bgLoadingColor, fadeDuration = '500ms' } = evaluate(expression || '{}');
if (!aspectRatio) {
throw new Error('`x-fade-in-img` AlpineJS directive requires an aspectRatio prop within the passed in object.');
}
const loadingColor = bgLoadingColor || '#E5E7EB';
const hasInlineObjectFit = el.style.objectFit;
const src = el.getAttribute('src');
el.src = '';
// Remove x-cloak, if it exists. This is needed so that
// the image, if cached, doesn't load bigger than necessary.
el.removeAttribute('x-cloak');
// Sets the loading background color.
setInitialImageBackground(el, loadingColor, aspectRatio);
setupImageReplacement(el, src, hasInlineObjectFit, Alpine, fadeDuration);
});
});
@cameronapak
Copy link
Author

AlpineJS Directive Fade in images on load

...instead of seeing the page flash or experience layout shift.

How To Use

  1. Add x-fade-in-img to your img elements.
  2. Ensure the img element has an aspect ratio. (i.e. x-fade-in-img="{ aspectRatio: '16/9' }")
  3. That's it!

Directive API

x-fade-in-img is given an object as a string. It contains "{ aspectRatio, bgLoadingColor, fadeDuration }"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment