DEV Community

Cover image for CleverImage Astro Component for My Responsive Images
Tömő Viktor
Tömő Viktor

Posted on • Originally published at tomoviktor.com

CleverImage Astro Component for My Responsive Images

Create responsive images in pure HTML with the CleverImage component. Let me show you how easy it is. The full code written in this post can be found at GitHub Gist: CleverImg.astro.

What is a responsive image?

Responsive images work well on all kinds of screen sizes and resolutions. It can help in optimizing page performance. For example, you don't need a 4k image on a mobile screen because it is just a waste.

It can be done in pure HTML. To create one, you have to modify an img element's attributes:

  • srcset: specify multiple image sources and their scale factors (note that if sizes isn't defined then the browser will decide which to display, that is why order matters here, the browser will use the first usable image it finds)
  • sizes: specify which image source to use from the srcset on which screen sizes.

These two attributes value generation will be implemented.

Example

In the previous part we created an automatic image generator, look at that for generating images, I am going to use an example related to that.

Let's say our base image is cat.png, it has different sizes and formats (file names scheme: [NAME]-[WIDTH]-[HEIGHT].[EXTENSION]): cat-100-500.png, cat-100-500.webp, cat-200-1000.png, cat-200-1000.webp. The smaller image should be loaded if the user's screen is smaller than 768px. WebP is preferred.

<img 
  src="cat-200-1000.png"
  alt="a black cat with big green eyes"
  srcset="cat-100-500.webp 100w,
          cat-100-500.png 100w,
          cat-200-1000.webp 200w,
          cat-200-1000.png 200w"
  sizes="(max-width: 768px) 100px, 200px"
/>
Enter fullscreen mode Exit fullscreen mode

For more explanation read MDN - Responsive images.

Implementation

Setup

Create a new Astro component, let's name it CleverImg.astro.

What props are needed?

  • Basic img tag arguments: imgPath (src), alt, loading (if later want to add lazy load)
  • sizes what tells the sizes to generate for srcset
  • breakpoints that declares at what maxWidth which size source should be loaded, here 0 maxWidth will be the default non media query related size
  • withWebp if the image(s) exist in .webp format too
interface Props {
  imgPath: string,
  alt: string,
  loading?: "eager" | "lazy" | null,
  sizes: { width: number; height: number }[],
  breakpoints?: {maxWidth: number, imgWidth: number}[]
  withWebp?: boolean,
}
const { imgPath, alt, sizes, withWebp, loading, breakpoints } = Astro.props; // init each prop as a const variable
Enter fullscreen mode Exit fullscreen mode

Both srcset and sizes require separating strings with a comma. That is why both will be handled as an array and will be joined after.

const generatedSrcset: string[] = [];
const generatedSizes: string[] = [];
Enter fullscreen mode Exit fullscreen mode

If you followed the previous part, and images are only generated on build, then put the following code into an if, so it only runs on build:

if (import.meta.env.PROD) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Generate srcset

First, sort the sizes. Because order matters, order them ascending.

sizes.sort((a, b) => a.width - b.width);
Enter fullscreen mode Exit fullscreen mode

Now, we have to append strings to generatedSrcset in format [IMGPATH] [IMGWIDTH]w. Path will be generated with function that was written in previous part of this series. This must be done for WebP format too.

for (const size of sizes) {
  ((withWebp ?? true) ? [true, false] : [false]).forEach((isWebp) => { // to do it for webp too
    generatedSrcset.push(`${ImageGenerator.generateName(imgPath, size, isWebp)} ${size.width}w`);
  });
}
Enter fullscreen mode Exit fullscreen mode

There is a small modification which should be made. If there is a breakpoint, only include images which are in that. Because breakpoint will tell the browser to display which image and when, just those images are required. For this, let's filter the sizes and remove ones that aren't required.

let allBreakpointSizes: number[] | null = null;
if (breakpoints) {
  allBreakpointSizes = breakpoints.map(bp => bp.imgWidth);
}
sizes.sort((a, b) => a.width - b.width);
for (const size of sizes) {
  if (allBreakpointSizes && allBreakpointSizes.indexOf(size.width) === -1) {continue;} // skip if the curren width is not found in breakpoints
  ((withWebp ?? true) ? [false, true] : [false]).forEach((isWebp) => {
    generatedSrcset.push(`${ImageGenerator.generateName(imgPath, size, isWebp)} ${size.width}w`);
  });
}
Enter fullscreen mode Exit fullscreen mode

Generate sizes

Add strings to generatedSizes in (max-width: [MAXWIDTH]px) {IMGWIDTH}px format. There is one exception, if the maxWidth is 0 then it means that this is the default value.

if (breakpoints) {
  for (const breakpoint of breakpoints) {
    if (breakpoint.maxWidth === 0) {
      generatedSizes.push(`${breakpoint.imgWidth}px`); // handle default
    } else {
      generatedSizes.push(`(max-width: ${breakpoint.maxWidth}px) ${breakpoint.imgWidth}px`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use it in HTML

The src should be the biggest sized image. If someone's browser doesn't support srcset then this will be used. The other attributes are nothing special, the generated arrays must be joined with comma.

---
// ...
const biggestSize = sizes.reduce((prev, current) => {
  return (prev.width > current.width) ? prev : current;
});
---
<img
  src={ImageCompressorIntegration.generateName(imgPath, biggestSize)}
  alt={alt}
  srcset={generatedSrcset.join(", ")}
  sizes={generatedSizes.join(", ")}
  loading={loading}
/>
Enter fullscreen mode Exit fullscreen mode

If you followed the previous part, and images are only generated on build, then put the fully generated img in a conditional, and put a simple img in with import.meta.env.DEV condition. Look at the implementation at GitHub Gist: CleverImg.astro.

Top comments (0)