Created
October 11, 2019 15:07
-
-
Save kmelve/e047d40e24d92f1b4751535a4cee1e59 to your computer and use it in GitHub Desktop.
Lazyload Images from Sanity.io
This file contains 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
.root { | |
display: block; | |
position: relative; | |
} | |
.lqip { | |
image-rendering: pixelated; | |
width: 100%; | |
opacity: 1; | |
transition: opacity 50ms 100ms ease-out; | |
@nest .root[data-has-aspect="true"] & { | |
display: block; | |
position: absolute; | |
top: 0; | |
left: 0; | |
height: 100%; | |
z-index: 0; | |
background-size: 100% 100%; | |
} | |
@nest &[data-is-loaded="true"] { | |
opacity: 0; | |
} | |
} | |
.img { | |
image-rendering: auto; | |
display: block; | |
width: 100%; | |
@nest .root[data-has-aspect="true"] & { | |
position: absolute; | |
top: 0; | |
left: 0; | |
height: 100%; | |
z-index: 1; | |
} | |
} |
This file contains 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
import React, {useState, useEffect} from 'react' | |
import PropTypes from 'prop-types' | |
import ImageUrlBuilder from './ImageUrlBuilder' | |
import {useInView} from 'react-intersection-observer' | |
import css from './Image.css' | |
import {get, omit} from 'lodash' | |
const DEFAULT_WIDTH = 500 | |
const DEFAULT_WIDTHS = [320, 480, 640, 800, 1440] | |
const DEFAULT_SIZES = `(max-width: 320px) 280px, (max-width: 480px) 440px, 800px` | |
// eslint-disable-next-line complexity | |
const Image = props => { | |
const [inViewRef, inView] = useInView({ | |
triggerOnce: true, | |
threshold: 0 | |
}) | |
const [isLoaded, setLoaded] = useState(0) | |
// When an image is in the browser cache or is completed loading before react rehydration, | |
// the `onload` may not be triggered. In order to ensure we have the correct "complete" | |
// state, check the `complete` property after mounting | |
const imgRef = React.createRef() | |
useEffect(() => { | |
if (imgRef && imgRef.current && imgRef.current.complete && imgRef.current.naturalWidth) { | |
setLoaded(true) | |
} | |
}) | |
const { | |
image, | |
alt, | |
src, | |
aspect, | |
srcSet, | |
width = DEFAULT_WIDTH, | |
fit = 'clip', | |
sizes = DEFAULT_SIZES, | |
widths = DEFAULT_WIDTHS, | |
lazy = 'true' | |
} = props | |
if (!src && (!image || !image.asset)) { | |
return <img /> | |
} | |
const showImage = lazy === 'false' || !!inView | |
const orgWidth = get(image, 'asset.metadata.dimensions.width') | |
const orgHeight = get(image, 'asset.metadata.dimensions.height') | |
const aspectRatio = aspect || orgWidth / orgHeight || null | |
const defaultSrcSetParts = aspectRatio | |
? widths.map( | |
sourceW => | |
`${ImageUrlBuilder(image) | |
.width(sourceW) | |
.height(Math.round(sourceW / aspectRatio)) | |
.fit(fit) | |
.url()} ${sourceW}w` | |
) | |
: widths.map( | |
sourceW => | |
`${ImageUrlBuilder(image) | |
.width(sourceW) | |
.url()} ${sourceW}w` | |
) | |
const defaultSrcSet = defaultSrcSetParts.join(',') | |
const height = aspectRatio ? Math.round(width / aspectRatio) : null | |
const computedSrc = ImageUrlBuilder(image) | |
.width(width) | |
// .height(height) | |
.fit(fit) | |
.url() | |
const className = [props.className, css.root].filter(Boolean).join(' ') | |
const bg = get(image, 'asset.metadata.palette.dominant.background') | |
const lqip = get(image, 'asset.metadata.lqip') | |
return ( | |
<div | |
className={className} | |
data-has-aspect={!!aspectRatio} | |
style={{ | |
paddingBottom: aspectRatio ? `${100 / aspectRatio}%` : undefined | |
}} | |
> | |
<div | |
ref={inViewRef} | |
className={css.lqip} | |
data-is-loaded={isLoaded} | |
aria-hidden="true" | |
style={{ | |
backgroundColor: bg, | |
backgroundImage: lqip && `url(${lqip})` | |
}} | |
/> | |
<img | |
{...omit(props, ['image', 'className', 'aspect', 'width', 'height', 'widths', 'lazy', 'clip'])} | |
alt={alt || (image && image.alt)} | |
ref={imgRef} | |
srcSet={showImage && !src ? defaultSrcSet || srcSet : undefined} | |
sizes={sizes} | |
src={showImage ? src || computedSrc : undefined} | |
className={css.img} | |
width={width} | |
height={height} | |
onLoad={() => setLoaded(true)} | |
style={{opacity: isLoaded ? 1 : 0}} | |
/> | |
</div> | |
) | |
} | |
Image.propTypes = { | |
image: PropTypes.object, | |
className: PropTypes.string, | |
alt: PropTypes.string, | |
aspect: PropTypes.number, | |
src: PropTypes.string, | |
fit: PropTypes.string, | |
srcSet: PropTypes.string, | |
sizes: PropTypes.string, | |
width: PropTypes.number, | |
lazy: PropTypes.string, | |
widths: PropTypes.arrayOf(PropTypes.number) | |
} | |
export default Image |
This file contains 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
import sanityClient from '../client.js' | |
import imageUrlBuilder from '@sanity/image-url' | |
const builder = imageUrlBuilder(sanityClient) | |
export default function urlFor(source) { | |
return builder.image(source).auto('format').fit('max') | |
} |
Nevermind. I figured it out. 🥳
Image.module.css
.root {
display: block;
position: relative;
}
.lqip {
image-rendering: pixelated;
width: 100%;
opacity: 1;
transition: opacity 50ms 100ms ease-out;
}
.root[data-has-aspect="true"] .lqip {
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
z-index: 0;
background-size: 100% 100%;
}
.lqip[data-is-loaded="true"] {
opacity: 0;
}
.img {
image-rendering: auto;
display: block;
width: 100%;
}
.root[data-has-aspect="true"] .img {
position: absolute;
top: 0;
left: 0;
height: 100%;
z-index: 1;
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've been trying to get this to work in Next.js for a few hours now. :o
I couldn't get the Image.css to work with @nest, so I just made regular
className
s, then I've tried everything from setting the width of the image to the container and playing around with the DOM. I just can't get the lqip to render at the size of the container. :|/app/page.js
/app/components/lqip/Image.js
/app/components/lqip/Image.css
Help 🙏
PS: After I posted this, I realized I left a @nest at the bottom so now I'm trying to figure out how to use @nest rule.