Table of Contents
Using <img />
and srcset
The following is a custom component for Astro that generates a responsive image using the srcset
and sizes
attributes. It also uses the getImage
function from the astro:assets
module to generate the image URLs. The function uses the same compression as the built-in <Image />
component from Astro.
For more information on responsive images, see the MDN article.
---
import type { ImageOutputFormat } from "astro";
import { getImage, type ImgAttributes } from "astro:assets";
type Unit = "px" | "em" | "rem";
type Size = `${number}${Unit}`;
type MinMediaQuery = `(min-width: ${Size})`;
type MaxMediaQuery = `(max-width: ${Size})`;
type OtherMediaQuery =
| "(orientation: portrait)"
| "(orientation: landscape)"
| "(prefers-color-scheme: dark)"
| "(prefers-color-scheme: light)";
type MediaQueries = MinMediaQuery | MaxMediaQuery | OtherMediaQuery;
type Props = Omit<ImgAttributes, "sizes"> & {
src: ImageMetadata;
format?: ImageOutputFormat;
srcSet: {
media?: `${MediaQueries}` | `(${MinMediaQuery} and ${MaxMediaQuery})` | `(${MaxMediaQuery} or ${MinMediaQuery})`;
size: number;
}[];
};
const { src, format, alt, id, srcSet } = Astro.props;
const className = Astro.props.class;
if (!srcSet.length) throw new Error("No srcSet provided");
const imgSrc = await getImage({ src, width: Math.min(...srcSet.map((ss) => ss.size)), format });
const srcset = await Promise.all(
srcSet.map(async (ss) => {
const image = await getImage({ src, width: ss.size, format });
return `${image.src} ${ss.size}w`;
})
);
const sizes = srcSet.map((ss) => (ss.media ? `${ss.media} ${ss.size}px` : `${ss.size}px`));
---
<img srcset={srcset.join(", ")} sizes={sizes.join(", ")} {id} {alt} src={imgSrc.src} class={className} loading="lazy" />
Example
// Post Cover Image
<ResponsiveImage
src={image}
alt={title}
id="post-image"
srcSet={[
{ media: "(max-width: 500px)", size: 500 },
{ media: "(max-width: 700px)", size: 700 },
{ size: 900 }
]}
class="h-full w-full object-cover object-center shadow shadow-black/50"
/>
// Post Card Image
<ResponsiveImage
src={post.data.image}
alt={post.data.title}
srcSet={[
{ media: "(min-width: 564px)", size: 424 },
{ size: 640 }
]}
class="h-full w-full object-cover object-center"
/>
The Result
<!-- Post Cover Image -->
<img srcset="/_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=500&f=webp 500w, /_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=700&f=webp 700w, /_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=900&f=webp 900w" sizes="(max-width: 500px) 500px, (max-width: 700px) 700px, 900px" id="post-image" alt="Astro Responsive Image" src="/_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=500&f=webp" class="h-full w-full object-cover object-center shadow shadow-black/50" loading="lazy">
<!-- Post Card Image -->
<img srcset="/_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=424&f=webp 424w, /_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=640&f=webp 640w" sizes="(min-width: 564px) 424px, 640px" alt="Astro Responsive Image" src="/_image?href=[redacted]%3ForigWidth%3D1600%26origHeight%3D840%26origFormat%3Dwebp&w=424&f=webp" class="h-full w-full object-cover object-center" loading="lazy">
Using <picture>
and <source>
The following is a custom component for Astro that generates a responsive image using the <picture>
element and <source>
elements. It also uses the getImage
function from the astro:assets
module to generate the image URLs. The function uses the same compression as the built-in <Image />
component from Astro.
---
import { cn } from "$/util";
import type { ImageOutputFormat } from "astro";
import { getImage, type ImgAttributes } from "astro:assets";
type Unit = "px" | "em" | "rem";
type Size = `${number}${Unit}`;
type MinMediaQuery = `(min-width: ${Size})`;
type MaxMediaQuery = `(max-width: ${Size})`;
type MinMaxMediaQuery =
| MinMediaQuery
| MaxMediaQuery
| `${MinMediaQuery} and ${MaxMediaQuery}`
| `(${MaxMediaQuery} or ${MinMediaQuery})`;
type OtherMediaQuery =
| "(orientation: portrait)"
| "(orientation: landscape)"
| "(prefers-color-scheme: dark)"
| "(prefers-color-scheme: light)";
type OMMMediaQuery = `${OtherMediaQuery} and ${MinMaxMediaQuery}`;
type Props = ImgAttributes & {
src: ImageMetadata;
pictureClass?: string | string[];
include2x?: boolean;
include3x?: boolean;
srcSet: {
src?: ImageMetadata;
media: MinMaxMediaQuery | OtherMediaQuery | OMMMediaQuery;
size: number;
format?: ImageOutputFormat;
}[];
};
const { src, srcSet, pictureClass, include2x, include3x, ...rest } = Astro.props;
const className = Astro.props.class;
const loading = Astro.props.loading || "lazy";
const srcset = (
await Promise.all(
srcSet.map(async ({ media, size, format, ...img }) => {
const url = src || img.src;
const images: string[] = [];
const image = await getImage({ src: url, width: size, format });
images.push(`${image.src}`);
if (include2x) {
const image2x = await getImage({ src: url, width: size * 2, format });
images.push(`${image2x.src} 2x`);
}
if (include3x) {
const image3x = await getImage({ src: url, width: size * 3, format });
images.push(`${image3x.src} 3x`);
}
return {
src: images.join(", "),
media,
type: format && `image/${format}`,
size
};
})
)
).sort((a, b) => a.size - b.size);
const maxSize = srcset.reduce((acc, curr) => (curr.size > acc ? curr.size : acc), 0);
const imgSrc = srcset.find((ss) => ss.size === maxSize);
if (!imgSrc) throw new Error("No image source found");
---
<picture class={cn("h-full w-full", pictureClass)}>
{srcset.map((ss) => <source srcset={ss.src} media={ss.media} type={ss.type} />)}
<img src={imgSrc.src.split(", ")[0]} loading={loading} class={className} {...rest} />
</picture>
Example
// Post Cover Image
<ResponsiveImage
src={image}
alt={title}
id="post-image"
srcSet={[
{ media: "(max-width: 500px)", size: 500 },
{ media: "(max-width: 700px)", size: 700 },
{ media: "(min-width: 701px)", size: 900 }
]}
class="h-full w-full object-cover object-center shadow shadow-black/50"
/>
// Post Card Image
<ResponsiveImage
src={post.data.image}
alt={post.data.title}
srcSet={[
{ media: "(min-width: 564px)", size: 424 },
{ media: "(max-width: 564px)", size: 640 }
]}
class="h-full w-full object-cover object-center"
/>
The Result
<!-- Post Cover Image -->
<picture class="h-full w-full">
<source srcset="/_astro/astro.b134230a_1zSOD8.webp" media="(max-width: 500px)">
<source srcset="/_astro/astro.b134230a_ZoRSGi.webp" media="(max-width: 700px)">
<source srcset="/_astro/astro.b134230a_Z2oEC1I.webp" media="(min-width: 701px)">
<img src="/_astro/astro.b134230a_Z2oEC1I.webp" loading="lazy" class="h-full w-full object-cover object-center shadow shadow-black/50" alt="Astro Responsive Image" id="post-image">
</picture>
<!-- Post Card Image -->
<picture class="h-full w-full">
<source srcset="/_astro/astro.b134230a_Z10wjS6.webp" media="(min-width: 564px)">
<source srcset="/_astro/astro.b134230a_Z24VRyK.webp" media="(max-width: 564px)">
<img src="/_astro/astro.b134230a_Z24VRyK.webp" loading="lazy" class="h-full w-full object-cover object-center" alt="Astro Responsive Image">
</picture>