-
-
Save TheThirdRace/7f270a786629f119b57d1b2227a4b113 to your computer and use it in GitHub Desktop.
/** | |
* # `<Image>` | |
* | |
* This component is a merge between `next/image` and `Chakra-ui`. | |
* - last updated on 2023-08-08 with `next/image` 13.4.13 and `chakra-ui/react` 2.8.0 | |
* - https://github.com/vercel/next.js/blob/v13.4.13/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/blob/canary/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/commits/canary/packages/next/src/client/image-component.tsx | |
* - https://github.com/vercel/next.js/compare/v13.4.4...canary | |
* | |
* Associated `gist`: <https://gist.github.com/TheThirdRace/7f270a786629f119b57d1b2227a4b113> | |
* | |
* ## Pros | |
* | |
* - Use NextJs backend solution so you get `static` or `on the fly` image optimization | |
* - Offer the same optimizations as `next/image` (lazy loading, priority, async decoding, no CLS, blur placeholder, etc.) | |
* - Use Chakra's theme (`variant`) so you have full control of styling | |
* - `<img>` is back to `display: inline-block` by default | |
* - Forward ref to `<img>` | |
* - No more fiddling with `onLoadComplete` callback from `next/image` | |
* - You can determine when an image is completely loaded | |
* - You can pass a callback `ref` and check if `data-loaded` is `true` | |
* - You can use `css` to target `[data-loaded=true]` | |
* - All icons are served automatically through the `1x` or `2x` pixel density optimization | |
* - Passing `sizesMax={0}` can force a bigger image to be served in the `1x` and `2x` mode | |
* - All images are served automatically through an `srcset` auto-build function | |
* - Load configs through `NextJs` config | |
* - No more fiddling trying to build a `sizes` manually | |
* - Simply pass `sizesMax={ImageMaxWidth}` or don't pass `sizesMax` at all (defaults to highest possible value) | |
* - `sizesMax` allows you to limit image width according to your design, not the viewport | |
* - No more loading a 3840px wide image on a 4K screen when your `main` section is 1200px | |
* - Use semantic HTML tags | |
* - `<img>` is used for the image | |
* - `<picture>` is used for the wrapper/container (optional) | |
* - `height` & `width` are extremely recommended, but not mandatory | |
* - Can use a blurry placeholder for better user experience and core vitals | |
* - Automatic when using static images (`import`) | |
* - You can manually pass a data uri for dynamic images | |
* - Low `height` and `width` images like icons won't apply the blurry placeholder as it lower performance | |
* - `loader` function allow to build the final `src` url, so you can support many image providers | |
* - Possible to use with a **secure** `Content-Security-Policy` header | |
* - Extra performance by using `content-visibility: auto` on the `<picture>` wrapper | |
* - Not available by default on `<img>` to avoid scrolling up issues on Chrome | |
* - Could be added manually on `<img>` through styles if wanted | |
* - Smaller than `next/image` solution by almost 200 lines of code | |
* - Smaller by almost 450 lines of codes if you count all the extra messages from development (which are loaded in PROD) | |
* | |
* ## Cons | |
* | |
* - Doesn't support Chakra's inline styling (by personal choice, could easily be added) | |
* - Using a different `backgroundSize`/`backgroundPosition` from default requires to style the `blur` placeholder | |
* - Use native `loading=lazy`, meaning the feature isn't supported for all browsers yet | |
* - Automatic blurry placeholder generation only works when your source image is a avif, jpg, png or webp | |
* - Same restrictions as NextJs since the component use their image optimization solution | |
* - Be advised, the "source" image is not the image served to your users, it's the unoptimized image before optimization | |
* - Using `<img>` without it's wrapper (`<picture>`) will give a very low CLS instead of none (ex: 0.03) | |
* - Serving "responsive" images can increase data consumption, but this should be negligible because: | |
* - Images are optimized to a low size to begin with | |
* - Those most affected are users with big screens, which usually don't mind more data | |
* - Users don't resize their browser window non-stop | |
* | |
* ## Tips & Tricks | |
* | |
* ### Optimization | |
* | |
* - Pass `width` & `height` whenever you can, it's the biggest optimization you're gonna get out of the box | |
* - Use `import` method for your images, it improves your Core Web Vitals and the user experience | |
* | |
* ### `<picture>` wrapper | |
* | |
* - Will be added automatically under these circumstances | |
* - Pass `width` & `height` props | |
* - Define a style for Image's `layPicture` part in the theme | |
* - `<picture>` wrapper is mandatory to reach a cumulative layout shift (CLS) of 0 | |
* - This implementation will always have a CLS of 0, no matter if it's a newer or older browser | |
* - The new `next/image` in NextJS `13.x` won't have 0 CLS, it'll get close on newer browser, but older browsers will have huge CLS | |
* - You won't be penalized by Google ranking as long as you keep CLS < 0.1, which makes the wrapper "optional" | |
* | |
* ### `sizesMax` | |
* | |
* - Pass `sizesMax={0}` to force an image to be optimized with `srcset` containing `1x, 2x` variants | |
* - Mostly for icons, but you could use this for normal images too | |
* - Don't pass `sizesMax` to force an image to be optimized for the current viewport width | |
* - If an image is less than the full screen's width, you can pass its max size like this `sizesMax={992}` | |
*/ | |
import { chakra } from '@chakra-ui/react' | |
import Head from 'next/head' | |
import { type ImageProps as NextImageProps, type StaticImageData } from 'next/image' | |
import { forwardRef, useImperativeHandle, useState, type Dispatch, type ReactElement, type SetStateAction } from 'react' | |
import { | |
defaultLoader, | |
useImageAttributes, | |
useImageOnLoad, | |
useImageStyle, | |
type GenerateImageAttributesReturn, | |
type ImageProps | |
} from '~/helper/Image' | |
import { Rename } from '~/shared/type/Typescript' | |
/** ******************************************************************************************************************* | |
* Types | |
*/ | |
type ImageNativeProps = Partial<Pick<HTMLImageElement, 'alt'>> & | |
Partial<Rename<Pick<HTMLImageElement, 'height'>, 'height', 'htmlHeight'>> & | |
Partial<Rename<Pick<HTMLImageElement, 'width'>, 'width', 'htmlWidth'>> & { | |
'data-set-load-state': Dispatch<SetStateAction<boolean>> | |
} | |
type ImagePriorityProps = Pick<NextImageProps, 'crossOrigin' | 'priority'> & | |
Pick<GenerateImageAttributesReturn, 'sizes' | 'src' | 'srcset'> | |
type StaticImageProps = Pick<StaticImageData, 'height' | 'src' | 'width'> & Pick<StaticImageData, 'blurDataURL'> | |
/** ******************************************************************************************************************* | |
* * Components * | |
*/ | |
const ImageNative = forwardRef<HTMLImageElement, ImageNativeProps>( | |
({ alt, htmlWidth, htmlHeight, 'data-set-load-state': setLoadState, ...chakraInternals }: ImageNativeProps, ref) => { | |
// Handle refs to the same element | |
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs) | |
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent | |
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages | |
// | |
// Inspired by | |
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 | |
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node | |
const { callbackRef, imgRef } = useImageOnLoad({ setLoadState }) | |
useImperativeHandle<HTMLImageElement | null, HTMLImageElement | null>(ref, () => imgRef.current) | |
return ( | |
// eslint-disable-next-line @next/next/no-img-element | |
<img | |
alt={alt} | |
height={htmlHeight} | |
ref={callbackRef} // ? use callback ref to catch when it updates | |
width={htmlWidth} | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...chakraInternals} | |
/> | |
) | |
} | |
) | |
const ImagePriority = ({ crossOrigin, sizes, src, srcset }: ImagePriorityProps): ReactElement => { | |
return ( | |
// Note how we omit the `href` attribute, as it would only be relevant | |
// for browsers that do not support `imagesrcset`, and in those cases | |
// it would likely cause the incorrect image to be preloaded. | |
// | |
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset | |
<Head> | |
<link | |
as='image' | |
crossOrigin={crossOrigin} | |
fetchpriority='high' // eslint-disable-line react/no-unknown-property | |
href={srcset ? undefined : src} | |
imageSizes={sizes} | |
imageSrcSet={srcset} | |
key={`__nimg-${src}${srcset}${sizes}`} | |
rel='preload' | |
/> | |
</Head> | |
) | |
} | |
export const Image = forwardRef<HTMLImageElement, ImageProps>( | |
( | |
{ | |
alt, | |
blurDataURL: paramBlurDataURL, | |
crossOrigin, | |
height: paramHeight, | |
loader = defaultLoader, | |
priority = false, | |
quality, | |
sizesMax, | |
src: paramSrc, | |
sx, | |
title, | |
variant, | |
width: paramWidth, | |
...chakraInternals | |
}: ImageProps, | |
ref | |
): ReactElement => { | |
// Manage values according to image mode: Static or Dynamic | |
const { blurDataURL, height, src, width } = | |
typeof paramSrc === 'string' | |
? { | |
blurDataURL: paramBlurDataURL, | |
height: paramHeight, | |
src: paramSrc, | |
width: paramWidth | |
} | |
: ({ | |
...paramSrc, | |
...(paramHeight ? { height: paramHeight } : {}), | |
...(paramWidth ? { width: paramWidth } : {}) | |
} as StaticImageProps) | |
// Keep trace of when the image is loaded | |
const [imgLoaded, setImgLoaded] = useState(false) | |
// Retrieve styling | |
const { styles, withWrapper } = useImageStyle({ blurDataURL, height, imgLoaded, src, variant, width }) | |
// Retrieve image attributes | |
const { | |
src: imgSrc, | |
srcset: imgSrcSet, | |
sizes: imgSizes | |
} = useImageAttributes({ | |
loader, | |
quality, | |
sizesMax, | |
src, | |
width | |
}) | |
// Image component | |
const imgProps = { | |
as: ImageNative, | |
alt, | |
decoding: 'async' as const, | |
...(priority ? { fetchpriority: 'high' } : { loading: 'lazy' as const }), | |
htmlHeight: height, | |
htmlWidth: width, | |
'data-set-load-state': setImgLoaded, | |
'data-loaded': imgLoaded, | |
ref, | |
// ? `src` must be the last parameter within those 3 | |
// ? Safari has a bug that would download the image in `src` before `sizes` and `srcset` | |
// ? are set and then download a second image when both are set. | |
// ? | |
// ? By putting `src` in last position, Safari won't initiate a download until `src` is | |
// ? updated in the DOM correctly, | |
sizes: imgSizes, | |
srcSet: imgSrcSet, | |
src: imgSrc, | |
// ? --------------------------------------------------------------------------------------, | |
sx: styles.image, | |
title, | |
// eslint-disable-next-line react/jsx-props-no-spreading, | |
...chakraInternals | |
} | |
const img = ( | |
<> | |
<chakra.img | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...imgProps} | |
/> | |
<noscript> | |
<chakra.img | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...imgProps} | |
sx={styles.imageNoScript} | |
/> | |
</noscript> | |
</> | |
) | |
// Add a `<picture>` wrapper if required | |
const image = withWrapper ? <chakra.picture sx={{ ...styles.picture, ...sx }}>{img}</chakra.picture> : img | |
return ( | |
<> | |
{image} | |
{priority ? ( | |
<ImagePriority crossOrigin={crossOrigin} sizes={imgSizes} src={imgSrc} srcset={imgSrcSet} /> | |
) : undefined} | |
</> | |
) | |
} | |
) |
import { useMultiStyleConfig, type ChakraProps, type SystemStyleObject, type ThemingProps } from '@chakra-ui/react' | |
import { mergeWith } from '@chakra-ui/utils' | |
import { imageConfigDefault, type ImageConfigComplete } from 'next/dist/shared/lib/image-config' | |
import { type ImageLoaderProps, type ImageProps as NextImageProps } from 'next/image' | |
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useMemo, useRef } from 'react' | |
/** ******************************************************************************************************************* | |
* Types | |
*/ | |
export type ImageProps = Pick<NextImageProps, 'blurDataURL' | 'crossOrigin' | 'priority' | 'src'> & | |
Partial<Pick<HTMLImageElement, 'alt' | 'height' | 'title' | 'width'>> & | |
Pick<ChakraProps, 'sx'> & | |
Pick<ThemingProps, 'variant'> & { | |
loader?: ImageLoaderWithConfig | |
quality?: number | |
sizesMax?: SizesMax | |
} | |
type GenerateCumulativeLayoutShiftFixProps = Pick<ImageProps, 'height' | 'sizesMax' | 'width'> | |
type GenerateImageAttributesProps = Required<Pick<ImageProps, 'loader'>> & | |
Pick<ImageProps, 'quality' | 'sizesMax' | 'width'> & | |
Pick<HTMLImageElement, 'src'> | |
export type GenerateImageAttributesReturn = Pick<HTMLImageElement, 'src'> & | |
Partial<Pick<HTMLImageElement, 'sizes' | 'srcset'>> | |
type ImageConfig = ImageConfigComplete & { allSizes: number[] } | |
type ImageLoaderWithConfig = (resolverProps: ImageLoaderPropsWithConfig) => string | |
type ImageLoaderPropsWithConfig = ImageLoaderProps & { | |
config: Readonly<ImageConfig> | |
} | |
type IsLayoutProps = Pick<ImageProps, 'sizesMax' | 'width'> | |
type UseImageOnLoadProps = { | |
setLoadState: Dispatch<SetStateAction<boolean>> | |
} | |
/** | |
* ! Makes sure `contentMaxWidthInPixel` from `page.ts` is included in `SizeMax` | |
* ! Makes sure values here are in sync with `next.config.js` | |
*/ | |
export type SizesMax = 0 | 320 | 480 | 640 | 750 | 828 | 992 | 1080 | 1200 | 1440 | 1920 | 2048 | 2560 | 3840 | |
type UseImageOnLoadReturn = { | |
callbackRef: (img: HTMLImageElement) => void | |
imgRef: MutableRefObject<HTMLImageElement | null> | |
} | |
type UseImageStyleProps = Pick<ImageProps, 'blurDataURL' | 'height' | 'sizesMax' | 'variant' | 'width'> & | |
Pick<HTMLImageElement, 'src'> & { | |
imgLoaded: boolean | |
} | |
type UseImageStyleReturn = { | |
styles: { | |
image: SystemStyleObject | |
imageNoScript: SystemStyleObject | |
picture: SystemStyleObject | |
} | |
withWrapper: boolean | |
} | |
/** ******************************************************************************************************************* | |
* * Image configurations * | |
* https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/image-config.ts | |
*/ | |
const defaultBlurDataURL = | |
// 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAA3NCSVQICAjb4U/gAAAAIElEQVQYlWNUUlJiwA1Y/v37h0/6////FOgeSMMpshsAm54bX5qzRrgAAAAASUVORK5CYII=', | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNU+g8AAUkBI5mqlHIAAAAASUVORK5CYII=' | |
const defaultQuality = 75 | |
const tmpConfig: ImageConfigComplete = mergeWith( | |
{}, | |
imageConfigDefault, | |
process.env.__NEXT_IMAGE_OPTS as unknown as ImageConfigComplete | |
) | |
const imageConfig: ImageConfig = mergeWith({}, tmpConfig, { | |
allSizes: [...tmpConfig.imageSizes, ...tmpConfig.deviceSizes].sort((a, b) => a - b) | |
}) | |
const { allSizes: configAllSizes, deviceSizes: configDeviceSizes, imageSizes: configImageSizes } = imageConfig | |
/** ******************************************************************************************************************* | |
* * Functions * | |
*/ | |
export const defaultLoader = ({ config, src, width, quality = defaultQuality }: ImageLoaderPropsWithConfig): string => | |
src.endsWith('.svg') && !config.dangerouslyAllowSVG | |
? src | |
: `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${quality}` | |
const isLayoutFixed = ({ sizesMax, width = configDeviceSizes[configDeviceSizes.length - 1] }: IsLayoutProps): boolean => | |
sizesMax === 0 || width < configDeviceSizes[0] || configImageSizes.includes(width) | |
const generateCumulativeLayoutShiftFix = ({ height, sizesMax, width }: GenerateCumulativeLayoutShiftFixProps) => { | |
let clsFix = {} | |
if (height && width) { | |
clsFix = { | |
aspectRatio: `${width}/${height}`, | |
...(isLayoutFixed({ sizesMax, width }) | |
? { | |
height: `${height}px`, | |
width: `${width}px` | |
} | |
: { | |
paddingBlockStart: `calc(${height} / ${width} * 100%)` | |
}) | |
} | |
} | |
return clsFix | |
} | |
export const useImageAttributes = ({ | |
loader, | |
quality, | |
sizesMax, | |
src, | |
width = configDeviceSizes[configDeviceSizes.length - 1] | |
}: GenerateImageAttributesProps): GenerateImageAttributesReturn => { | |
return useMemo(() => { | |
let imgAttributes: GenerateImageAttributesReturn | |
if (src && (src.startsWith('data:') || src.startsWith('blob:'))) { | |
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | |
imgAttributes = { src, srcset: undefined, sizes: undefined } | |
} else if (isLayoutFixed({ sizesMax, width })) { | |
const widths = [ | |
...new Set( | |
/** | |
* This means that most OLED screens that say they are 3x resolution, are actually 3x in the green color, | |
* but only 1.5x in the red and blue colors. | |
* | |
* Showing a 3x resolution image in the app vs a 2x resolution image will be visually the same, though the | |
* 3x image takes significantly more data. Even true 3x resolution screens are wasteful as the human eye | |
* cannot see that level of detail without something like a magnifying glass. | |
* | |
* https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html | |
*/ | |
[width, width * 2].map( | |
(w) => configAllSizes.find((s) => s >= w) || configAllSizes[configAllSizes.length - 1] | |
) | |
) | |
] | |
imgAttributes = { | |
sizes: undefined, | |
src: loader({ config: imageConfig, src, quality, width: widths[1] }), | |
srcset: widths | |
.map((w, i) => `${loader({ config: imageConfig, src, quality, width: w })} ${i + 1}x`) | |
.join(', ') | |
} | |
} else { | |
const maxSizes = sizesMax || configDeviceSizes[configDeviceSizes.length - 1] | |
const widths = [...configDeviceSizes.filter((w) => w < maxSizes), maxSizes] | |
imgAttributes = { | |
sizes: widths | |
.map((w, i) => { | |
return i < widths.length - 1 ? ` (max-width: ${w}px) ${w}px` : ` ${w}px` | |
}) | |
.join(','), | |
src: loader({ config: imageConfig, src, quality, width: widths[widths.length - 1] }), | |
srcset: widths.map((w) => `${loader({ config: imageConfig, src, quality, width: w })} ${w}w`).join(', ') | |
} | |
} | |
return imgAttributes | |
}, [loader, quality, sizesMax, src, width]) | |
} | |
export const useImageOnLoad = ({ setLoadState }: UseImageOnLoadProps): UseImageOnLoadReturn => { | |
// Handle refs to the same element | |
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs) | |
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent | |
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages | |
// | |
// Inspired by | |
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 | |
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node | |
const imgRef = useRef<HTMLImageElement | null>(null) | |
const callbackRef = useCallback( | |
// ? Because the page could be static rendered, the image could already be loaded before React registers the image's `onload` event, meaning it would never fire | |
// ? That's why we use a `ref` handler instead, see https://stackoverflow.com/q/39777833/266535 | |
(img: HTMLImageElement) => { | |
// Check if a node is actually passed. Otherwise node would be null. | |
if (img) { | |
// You can now do what you need to, addEventListeners, measure, etc. | |
const handleLoad = () => { | |
if (!img.src.startsWith('data:') && !img.src.startsWith('blob:')) { | |
const p = 'decode' in img ? img.decode() : Promise.resolve() | |
p.catch(() => {}) | |
.then(() => setLoadState(true)) | |
.catch(() => {}) | |
} | |
} | |
if (img.complete) { | |
// ? If the real image fails to load, this will still remove the blurred image | |
handleLoad() | |
} else { | |
img.onload = handleLoad // eslint-disable-line no-param-reassign, unicorn/prefer-add-event-listener | |
} | |
} | |
imgRef.current = img // Save a reference to the node | |
}, | |
[setLoadState] | |
) | |
return { | |
callbackRef, | |
imgRef | |
} | |
} | |
export const useImageStyle = ({ | |
blurDataURL = defaultBlurDataURL, | |
height, | |
imgLoaded, | |
sizesMax, | |
src, | |
variant, | |
width | |
}: UseImageStyleProps): UseImageStyleReturn => { | |
// Retrieve styles from theme | |
const { layPicture, layPictureCls, layImage, layImageCls, layImageNoScript, preImage, preImageBlur, preImageCls } = | |
useMultiStyleConfig('Image', { variant }) | |
// Do we need a wrapper? | |
const withWrapperFromProps = !!(width && height) | |
const withWrapperFromTheme = !!(layPicture && layPicture.constructor === Object && Object.keys(layPicture).length) | |
// Do we need a blur placeholder? | |
const withBlurPlaceholder = !!( | |
!imgLoaded && | |
blurDataURL && | |
(!height || height > 48) && | |
(!width || width > 48) && | |
!src.startsWith('data:') && | |
!src.startsWith('blob:') | |
) | |
return { | |
styles: { | |
image: { | |
// Styles for `<img>` when used with `<picture>` wrapper | |
// if wrapper is activated by theme then `variant` can override styles from wrapper | |
// if wrapper is activated by props then styles from wrapper will override `variant` | |
...(withWrapperFromTheme ? { ...layImageCls, ...preImageCls } : {}), | |
...layImage, | |
...preImage, | |
...(withWrapperFromProps ? { ...layImageCls, ...preImageCls } : {}), | |
...(withBlurPlaceholder | |
? { | |
'--blurBackgroundImage': `url("${blurDataURL}")`, | |
...preImageBlur | |
} | |
: {}) | |
}, | |
imageNoScript: { | |
...layImageNoScript | |
}, | |
picture: { | |
...generateCumulativeLayoutShiftFix({ height, sizesMax, width }), | |
// Styles for `<picture>` wrapper | |
// if wrapper is activated by theme then `variant` can override styles from wrapper | |
// if wrapper is activated by props then styles from wrapper will override `variant` | |
...(withWrapperFromTheme ? layPictureCls : {}), | |
...layPicture, | |
...(withWrapperFromProps ? layPictureCls : {}) | |
} | |
}, | |
withWrapper: withWrapperFromProps || withWrapperFromTheme | |
} | |
} |
import { type ComponentMultiStyleConfig } from '@chakra-ui/react' | |
import { anatomy, type PartsStyleObject } from '@chakra-ui/theme-tools' | |
const partsAnatomy = anatomy('image').parts( | |
'layPicture', | |
'layPictureCls', | |
'layImage', | |
'layImageCls', | |
'layImageNoScript', | |
'preImage', | |
'preImageBlur' | |
) | |
export type ImageStyleObject = PartsStyleObject<typeof partsAnatomy> | |
export const Image: ComponentMultiStyleConfig = { | |
parts: [], | |
baseStyle: { | |
layPicture: {}, | |
layPictureCls: { | |
// layout | |
display: 'block', // necessary for firefox | |
position: 'relative', | |
// box model | |
boxSizing: 'border-box', | |
// misc | |
contentVisibility: 'auto', | |
overflow: 'hidden' | |
}, | |
layImage: { | |
// layout | |
display: 'inline-block' | |
}, | |
layImageCls: { | |
// layout | |
display: 'block', // necessary for firefox | |
inset: 0, | |
position: 'absolute' | |
}, | |
layImageNoScript: { | |
// layout | |
position: 'absolute', | |
top: 0 | |
}, | |
preImage: { | |
// box model | |
height: 'auto', | |
maxWidth: 'inherit', | |
width: 'auto' | |
// misc | |
// ! not activated because it cause jumpiness while scrolling up in Chrome | |
// contentVisibility: 'auto' | |
// containIntrinsicSize: 'width height' // obviously need to be adjusted | |
}, | |
preImageBlur: { | |
// visual | |
backgroundImage: 'var(--blurBackgroundImage)', | |
backgroundPosition: '0% 0%', | |
backgroundSize: 'cover', | |
filter: 'blur(1.25rem)' | |
}, | |
preImageCls: { | |
// box model | |
maxHeight: '100%', | |
maxWidth: '100%', | |
minHeight: '100%', | |
minWidth: '100%' | |
} | |
}, | |
variants: {}, | |
defaultProps: {} | |
} |
images: { | |
/** | |
* ! Highly suggested to have your max content width defined here (it will better optimize the image size) | |
* ! For example, on my website, an image is at most 992px which is the centered part of the viewport where I put content | |
* ! Makes sure values here are in sync with `helper/Image.ts` | |
*/ | |
deviceSizes: [320, 480, 640, 750, 828, 992, 1080, 1200, 1440, 1920, 2048, 2560, 3840], | |
domains: [], | |
formats: ['image/avif', 'image/webp'], | |
minimumCacheTTL: 86400 // if `no max-age` or `s-max-age` defined for an image, cache it `1 day` | |
}, |
// ex: Rename<NextLinkProps, 'as', 'asRoute'> | |
export type Rename<T, K extends keyof T, N extends string> = Pick<T, Exclude<keyof T, K>> & { [P in N]: T[K] } |
@ifxli
I also realize I might need to update this Gist to latest version because I fixed a couple stuff when integrating changes from NextJs 11.1.2
Edit: I just updated the Gist to latest version including NextJs 11.1.2
It should solve your width
& height
problem, there was a bug in the previous version where the image would not respect the width
and height
while using import
@TheThirdRace thank you for the update.
I obviously had the problem of setting the width and height.
@TheThirdRace could you upload the helper/Lifecycle file as well?
@ifxli Just updated the helper/Lifecycle.ts
for you.
Originally, everything was mostly in the same file. As I ported more and more features, I had to separate stuff in multiple files to keep it tidy. I also made a big refactor at some point, which created new files too...
So thanks for pointing out which files I was missing in the gist. It also gave me the nudge to update it with the latest version of Chakra and NextJs.
@TheThirdRace Any chance this will be released into the wild as it's own component?
@bline Yes, but not any time soon :(
Given my repo is private, it makes it very hard to share public packages. Or at least, I think it does... I don't have much experience in creating packages 😅
If I can find a quick and easy way to simply move all my components to a separate package without impacting my private repo, I would definitely proceed this way. The only reason I keep my repo private is for some proprietary content (business logic), I would gladly share all my components as a public library if I can and it doesn't give me headaches to manage.
@ifxli
Even though I
import
the file, the truth is the file on disk is usually not the size you need. That's why you set thewidth
andheight
.The advantages of going with
import
method is you get the best caching mechanism (immutable) and the blur placeholder for free.If you're satisfied with the 2nd best caching mechanism (Etag) and no blur placeholder, you could use a plain URL for the
src
: