Skip to content

Instantly share code, notes, and snippets.

@monecchi
Created February 26, 2021 14:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monecchi/f6d73655f47e07be1cc00b469cfa93b6 to your computer and use it in GitHub Desktop.
Save monecchi/f6d73655f47e07be1cc00b469cfa93b6 to your computer and use it in GitHub Desktop.
Integrate NextJs `next/image` with Chakra-UI styling
/**
* ! Important in optimizing images
*
* Keep the values in sync between:
* - `deviceSizes` in `next.config.js`
* - `deviceSizes` in `image.ts`
*
* ! Recommended
* NextJs optimize images according to your viewport. This is wonderful for mobile, but for desktop with a 4k screen, NextJs would
* download the 3840px version of your image.
*
* To workaround this unfortunate situation, I highly recommend you pass `size` to images with the highest width value being the
* max width an image can have on your site.
*
* For example, content on my site is centered and cannot be more than 960px wide, thus I make sure that 960 is in `deviceSizes` and
* I use `Sizes.main` to limit the image to only 960px. This considerably reduce the size in KB of my images and they're much
* better optimized on screens larger than 960px.
*
* This file is a way to generate the strings to pass to Image's `size` property and put the results in an Enum for easier consumption
*/
const deviceSizes = [320, 480, 640, 750, 828, 960, 1080, 1200, 1440, 1920, 2048, 2560, 3840]
const deviceSizesMax = Math.max(...deviceSizes)
/**
* ? `generateSizes` will create the strings necessary for `Sizes` enum
*
* ? Simply uncomment the `console.log` and adjust values
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const generateSizes = (upperLimit: number = deviceSizesMax): string => {
const sizes = [...deviceSizes.filter((v) => v < upperLimit), upperLimit]
return sizes
.map((v, i) => {
return i < sizes.length - 1 ? ` (max-width: ${v}px) ${v}px` : ` ${v}px`
})
.join()
}
// console.log(generateSizes(960)) // I use a variable, but since it's easier to understand with a real number...
// console.log(generateSizes())
export enum Sizes {
main = '(max-width: 320px) 320px, (max-width: 480px) 480px, (max-width: 640px) 640px, (max-width: 750px) 750px, (max-width: 828px) 828px, 960px',
full = '(max-width: 320px) 320px, (max-width: 480px) 480px, (max-width: 640px) 640px, (max-width: 750px) 750px, (max-width: 828px) 828px, (max-width: 960px) 960px, (max-width: 1080px) 1080px, (max-width: 1200px) 1200px, (max-width: 1440px) 1440px, (max-width: 1920px) 1920px, (max-width: 2048px) 2048px, (max-width: 2560px) 2560px, 3840px'
}
import { chakra, ThemingProps, useStyleConfig } from '@chakra-ui/react'
import NextImage, { ImageProps as NextImageProps } from 'next/image'
import { ReactElement } from 'react'
import { Sizes } from '../../theme/variables/image'
// TODO review props when NextJs is updated so we don't have to defined it here
/**
* ? Because NextJs typing is preventing auto-suggest for layout, width and height,
* ? we declare the styles differently in this component and will manage the switch
* ? to NextJs typings when calling NextJs `next/image` component
*/
type LayoutValue = 'fixed' | 'intrinsic' | 'responsive' | undefined
type LayoutAndSize =
| {
layout: 'fill'
}
| {
layout: LayoutValue
height: number
width: number
}
/**
* Types for the Image component itself
*/
type ImageProps = Pick<
NextImageProps,
'className' | 'loading' | 'objectFit' | 'objectPosition' | 'priority' | 'quality' | 'src' | 'unoptimized'
> &
Pick<Required<NextImageProps>, 'alt'> &
Pick<ThemingProps, 'variant'> & {
dimensions?: [number, number]
layout?: 'fill' | LayoutValue
sizes?: Sizes // could be a string too, this one is just a way to make it easier
}
/**
* Wraps NextJs `next/image` component in Chakra's factory function
* This is what will allow to use the theme and the styling properties on the component
*/
const ImageWithChakra = chakra(
({
className,
dimensions = [0, 0],
layout = 'fill',
loading,
objectFit,
objectPosition,
priority,
quality,
sizes,
src,
unoptimized,
...nextjsInternals
}: ImageProps): ReactElement => {
/**
* ? As explained earlier, NextJs typing is preventing auto-suggest for layout, width and height
* ? Here we actually convert our component typing to NextJs typing
*/
const [width, height] = dimensions
const layoutAndSize: LayoutAndSize =
height > 0 || width > 0
? {
height,
layout: layout === 'fill' ? 'intrinsic' : layout,
width
}
: {
layout: 'fill'
}
return (
<NextImage
className={className}
loading={loading}
objectFit={objectFit}
objectPosition={objectPosition}
priority={priority}
quality={quality}
sizes={sizes}
src={src}
unoptimized={unoptimized}
// eslint-disable-next-line react/jsx-props-no-spreading
{...layoutAndSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...nextjsInternals}
/>
)
}
)
export const Image = ({ variant, ...props }: ImageProps): ReactElement => {
/**
* ? This components serves as an interface to pass Chakra's styles
* ? You can use the theme and/or styling properties (eg. backgroundColor='red.200')
*/
const styles = useStyleConfig('Image', { variant })
// eslint-disable-next-line react/jsx-props-no-spreading
return <ImageWithChakra sx={styles} {...props} />
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment