Last active
December 16, 2023 05:53
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
AlpineJS Directive Fade in images on load
...instead of seeing the page flash or experience layout shift.
How To Use
x-fade-in-img
to yourimg
elements.img
element has an aspect ratio. (i.e.x-fade-in-img="{ aspectRatio: '16/9' }"
)Directive API
x-fade-in-img
is given an object as a string. It contains"{ aspectRatio, bgLoadingColor, fadeDuration }"