an astronaut in the background, in the foreground there is a brain icon and an image icon

CleverImage Astro Component for My Responsive Images

Published on:

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:

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"
/>

For more explanation read MDN - Responsive images.

Implementation

Setup

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

What props are needed?

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

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[] = [];

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) {
  // ...
}

Generate Srcset

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

sizes.sort((a, b) => a.width - b.width);

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`);
  });
}

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`);
  });
}

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`);
    }
  }
}

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}
/>

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.

Read more

a futuristic background, 7 image icons next to each other, the image icons are getting smaller and smaller

Automatic Responsive Image Generation with an Astro Hook

a galaxy background, lots of image icons laying around, in the center there is a rocket launching up

I Boosted the Site's Performance with Better Images