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:
srcset
: specify multiple image sources and their scale factors (note that ifsizes
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 thesrcset
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"
/>
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 forsrcset
breakpoints
that declares at whatmaxWidth
which size source should be loaded, here0
maxWidth
will be the default non media query related sizewithWebp
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
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.
Previous Part