Skip to content

Instantly share code, notes, and snippets.

@jeromeabel
Last active August 17, 2025 21:29
Show Gist options
  • Select an option

  • Save jeromeabel/8716dc690339e3c1fea6fc324c564100 to your computer and use it in GitHub Desktop.

Select an option

Save jeromeabel/8716dc690339e3c1fea6fc324c564100 to your computer and use it in GitHub Desktop.
Responsive images with srcset and sizes attributes

Responsive images

Aka "Do we need a PhD to add images on websites ?"

I focus here on reponsive images. Loading and accessibility topics are not detailed. For the purpose of the article, I've just used the <img> markup, but for real use cases, <picture> will fill all the needs.

Goal: to display elements consistently on screens with different densities

Setup

To test the following sections, you need to open Development Tools, and play with different screen sizes. Be aware that the browser keeps the last image in a memory cache, you should disable the cache or refresh the page to see your expected results.

Another usefool tools to mention :

  • You can inspect the network tab to see what image is loaded
  • You can also select your element with the inspector, and see the sizes when you click on the element and the mouse go over the associated HTML code.

Simple img

<img src="640.jpg" alt="" width="640" height="480">

It's ok, but the size is fixed. For smaller screens (< 640px), the image is truncated. And if the image is inside a smaller container, the image exceed it.

Constrained and fluid width

 img { 
  display: block; /* Make it block to change width */
  max-width: 100%; /* Fluid on smaller screens. Avoid stretching artefacts for screens bigger */
  height: auto; /* Don't stretch the image, respect the ratio */
}

Now, the image is fluid on smaller screens. But what about all other screens : tablet, desktop ?

Srcset

<img 
        src="640.jpg" 
        srcset="640.jpg 640w, 
                768.jpg 768w,
                1024.jpg 1024w,
                1280.jpg 1280w,
                1536.jpg 1536w,
                2048.jpg 2048w,
                2560.jpg 2560w,
                3072.jpg 3072w"
        alt=""
    >

The browser can now choose the image according to the size of the window. But there is a tricky hidden notion here : the device pixel ratio (DPR). On my browser, for a screen of 1024px, the image loaded is 2048px ...

responsive_srcset

The best image is 2048px instead of 1024px because my DPR is 2. It means for one standard pixel, my browser can display 4. See this explanation from Understanding the Device Pixel Ratio:

image

In fact, the srcset attribute allows the browser to choose the image according to the size AND the pixel ratio of the current device, thanks to the w descriptor. That's why it is a very useful and sophisticated tool.

To know you DPR, two possibilities as I know :

  • write window.devicePixelRatio on the Console (DevTools)
  • or in DevTools, display the aspect ratio, in the "three dots" menu :

responsive_pixelratio

The downsides :

  • For mobile devices, the DPR might be 2 or more. The output is quite counterintuitive: they have less power but they might load bigger images...
  • Also, there might be some sizes differences between the choosen image and the expective one, due to the finite and discret list of sizes. For a screen of 1401 px, the image loaded is 2560px; for a screen of 1402px, the image loaded is 3072, instead of 2804px.
  • There is another sizes attribute that we don't use yet. By default, the value is "100vw". Here, for a very big screen of 2560px, the image loaded will be 3072px, the last bigger image of my list, instead of 2560px*2=5120px. The result might be blurry...

In real use case, it is very rare to display an image without a container which will adapt its content to the differents screen sizes.

Srcset with a container

Add a minimal container to the CSS :

.container {
	max-width: 1536px;
        margin-inline: auto; /* center */
}

And insert the img inside a container element:

<main class="container">
	<img src="..." srcset="..." alt="" >
</main>

Now, the image is not stretched for bigger screens.

The typical use case of this image might be the header section of an article or a hero page. Often, we need also some flex or grid layouts with one, two, or more columns depending of the screen sizes. Let's see what happens.

Simple 2 columns grid layout

.grid {
	display: grid;
        grid-template-columns: repeat(2, 1fr);
}

And insert two images inside a grid section :

<main class="container">
    <section class="grid">
    <img 
        src="640.jpg" 
        srcset="640.jpg 640w,
                768.jpg 768w,
                1024.jpg 1024w,
                1280.jpg 1280w,
                1536.jpg 1536w,
                2048.jpg 2048w,
                2560.jpg 2560w,
                3072.jpg 3072w"
        alt="">
        
    <img 
        src="640.jpg" 
        srcset="640.jpg 640w,
                768.jpg 768w,
                1024.jpg 1024w,
                1280.jpg 1280w,
                1536.jpg 1536w,
                2048.jpg 2048w,
                2560.jpg 2560w,
                3072.jpg 3072w"
        alt=""
    >
    </section>
</main>

The output is not optimized at all. We might thought that for a screen of 1280px with two columns my browser choose the 1280.jpg image, but as I mentioned earlier, it take by default "100vw", so we have two images loaded of 2560px.

The solution is to use the "sizes" attribute with : sizes="50vw". Now the images loaded are 1280px. It's better. But, what if our layout is reponsive, like the majority of current sites.

Responsive grid layout

We would like one column for screens smaller than 1024px and two columns for bigger ones.

.grid {
	display: grid;
	grid-template-columns: repeat(1, 1fr);
}

/* Mobile First */
@media (min-width: 1024px) {
	.grid { 
		grid-template-columns: repeat(2, 1fr);
	}
}

If we leave the parameter as it is, the image size is always "50vw", even if the grid is in one column for smaller devices. We need to add a media query inside the sizes attribute to choose 100% instead of 50%.

Here the implementation:

sizes="(max-width: 1023px) 100vw, 50vw"

The result is not optimal for big screens. For a 2536px screen, with my container max-width of 1536px and 2 columns, we should expect an image around 1536px (in DPR 2), but we have 2 images of 2536px (50vw).

We could constrain the bigger screen after the max-width of the container

sizes="(max-width: 1023px) 100vw, (max-width: 1536px) 50vw, 768px"

Final thoughts of the quest

For performance optimization, the customization of <img> or <picture> should depend on responsive breakpoints, using different set of image sizes. Here, we have to find the balance between details optimizations and usability.

Third Party Examples

Some implementations of responsive images.

Vercel

<img 
        alt="" 
        loading="lazy"
        width="700" 
        height="475" 
        decoding="async"
        style="color:transparent;width:100%;height:auto" 
        sizes="100vw"
        srcset="640.jpg 640w, 
        750.jpg 750w, 
        828.jpg 828w, 
        1080.jpg 1080w, 
        1200.jpg 1200w, 
        1920.jpg 1920w, 
        2048.jpg 2048w, 
        3840.jpg 3840w" 
        src="3840.jpg"
/>

unpic

<img
  alt=""
  loading="lazy"
  decoding="async"
  sizes="(min-width: 800px) 800px, 100vw"
  srcset="
    1600.jpg 1600w,
    1280.jpg 1280w,
    1080.jpg 1080w,
    960.jpg 960w,
    828.jpg 828w,
    800.jpg 800w,
    750.jpg 750w,
    640.jpg 640w
  "
  src="800.jpg"
  style="
        object-fit: cover;
        max-width: 800px;
        max-height: 600px;
        aspect-ratio: 1.33333 / 1;
        width: 100%;"
/>

Builder.io (blog)

<picture>
  <source type="image/avif" srcset="100.avif 100w, 200.avif 200w, 400.avif 400w, 800.avif 800w" />
  <source type="image/webp" srcset="100.webp 100w, 200.webp 200w, 400.webp 400w, 800.webp 800w" />
  <img src="image.png" srcset="100.png 100w, 200.png 200w, 400.png 400w, 800.png 800w"
    sizes="(max-width: 800px) 100vw, 50vw"
    style="width: 100%; aspect-ratio: 4/3"
    loading="lazy"
    decoding="async"
    alt=""
  />
</picture>

WordPress Reference

<picture>
	<!-- Large art direction -->
	<source media="(min-width: 800px)"
		srcset="medium.jpeg 600w,
				large.jpeg 1000w,
				extra-large.jpeg 1200w,
				extra-extra-large.jpeg 1500w,
				super-large.jpeg 2000w"
		sizes="(min-width: 60rem) 60rem, 100vw">
	<!-- Small art direction -->
	<source 
		srcset="tiny.jpeg 150w,
				small.jpeg 300w,
				medium.jpeg 600w,
				large.jpeg 1000w"
		sizes="100vw">	
	<!-- Fallback image for browsers that don't support the picture element -->
	<img src="medium.jpeg 600w"
		srcset="tiny.jpeg 150w,
				small.jpeg 300w,
				medium.jpeg 600w,
				large.jpeg 1000w,
				extra-large.jpeg 1200w,
				extra-extra-large.jpeg 1500w,
				super-large.jpeg 2000w"
		sizes="(min-width: 60rem) 60rem, 100vw">
</picture>

IE11 Fallback

<picture>
  <source srcset="image-large.webp" media="(min-width: 1200px)" type="image/webp">
  <source srcset="image-medium.webp" media="(min-width: 768px)" type="image/webp">
  <source srcset="image-small.webp" type="image/webp">
  <!--[if IE 11]>
    <img src="image-fallback.jpg" alt="Fallback Image">
  <![endif]-->
  <img src="image-small.jpg" alt="Small Image">
</picture>

References

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Responsive Images</title>
<style>
*,
*::after,
*::before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #5e5e5e;
color:#fff;
}
img {
max-width: 100%;
height: auto;
display: block;
}
.container {
max-width: 1536px;
margin-inline: auto;
padding: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(1, 1fr);
grid-gap: 2rem;
}
/* Mobile First */
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<main class="container">
<section class="grid">
<img
src="640.jpg"
srcset="640.jpg 640w,
768.jpg 768w,
1024.jpg 1024w,
1280.jpg 1280w,
1536.jpg 1536w,
2048.jpg 2048w,
2560.jpg 2560w,
3072.jpg 3072w"
alt=""
sizes="(max-width: 1023px) 100vw, (max-width: 1536px) 50vw, 768px"
>
<img
src="640.jpg"
srcset="640.jpg 640w,
768.jpg 768w,
1024.jpg 1024w,
1280.jpg 1280w,
1536.jpg 1536w,
2048.jpg 2048w,
2560.jpg 2560w,
3072.jpg 3072w"
alt=""
sizes="(max-width: 1023px) 100vw, (max-width: 1536px) 50vw, 768px"
>
</section>
</main>
</body>
</html>
#!/bin/bash
# Use "ffmpeg" to generate jpeg images with different sizes.
# Create the output directory
output_dir="build"
mkdir -p $output_dir
# Screen sizes : Tailwind : 640, 768, 1024, 1280, 1536 with ratio 1.33333
sizes=( "640x480" "768x576" "1024x768" "1280x960" "1536x1152" "2048x1536" "2560x1920" "3072x2304" )
# Pastel colors
colors=( "LavenderBlush" "PeachPuff" "LemonChiffon" "SkyBlue" "Thistle" "PowderBlue" "PaleGreen" "Plum" )
# Font
font="/home/<USER>/.local/share/fonts/Roboto-Black.ttf"
# Main loop
for i in "${!sizes[@]}"; do
size="${sizes[i]}"
bg_color="${colors[i]}"
width=$(echo $size | cut -d'x' -f1)
text="$width"
fontsize=36
ffmpeg -f lavfi -i color="$bg_color" -filter_complex "[0:v]drawtext=fontfile=$font:text='$size':fontcolor=black:fontsize=$fontsize:x=(w-tw)/2:y=(h-th)/2[v]" -map "[v]" -frames:v 1 -s "$size" "$output_dir/$width.jpg"
done

By ChatGPT

Device pixels: These are the physical pixels on a screen. Each pixel on a screen represents a small dot of light that can display a specific color. Device pixels are fixed in size and cannot be resized.

CSS pixels: These are virtual pixels that are used in CSS to define the size and layout of web content. The size of a CSS pixel is not fixed and can vary depending on the device's pixel density (also known as pixel ratio). For example, on a device with a pixel density of 2, one CSS pixel will be displayed as four device pixels (2 pixels horizontally and 2 pixels vertically).

Density-independent pixels (DP or DIP): These are virtual pixels that are used to define the size of images in a way that is independent of the device's pixel density. One DP is equal to one CSS pixel on a device with a pixel density of 160 dots per inch (dpi). For devices with higher pixel densities, the browser will automatically scale the image so that it appears the same physical size on the screen, but with more device pixels.

#!/bin/bash
# Generate differents sizes of all images of the "in" directory, and a blurry image used as placeholder.
# Define input directory and output directory
INPUT_DIRECTORY="in"
OUTPUT_DIRECTORY="out"
# Define image sizes
SIZES=("320x" "480x" "640x" "960x" "1280x")
# Create output directory if it doesn't exist
mkdir -p $OUTPUT_DIRECTORY
# Loop through each file in the input directory
for FILENAME in $INPUT_DIRECTORY/*; do
# Get the filename and extension
BASENAME=$(basename "$FILENAME")
EXTENSION="${BASENAME##*.}"
FILENAME_NO_EXT="${BASENAME%.*}"
# Loop through each size and generate the corresponding image
for SIZE in "${SIZES[@]}"; do
# Generate the resized image
convert "$FILENAME" -resize $SIZE "$OUTPUT_DIRECTORY/$FILENAME_NO_EXT"_"$SIZE.$EXTENSION"
# Generate the blurred image for the medium size
if [ "$SIZE" == "640x" ]; then
convert "$OUTPUT_DIRECTORY/$FILENAME_NO_EXT"_"$SIZE.$EXTENSION" -blur 0x8 "$OUTPUT_DIRECTORY/$FILENAME_NO_EXT"_"$SIZE-blur.$EXTENSION"
fi
done
done
#!/bin/bash
# Generate a pixelize image with ffmpeg
VAL=100
IN_FILE="in.jpg"
OUT_FILE="out.jpg"
ffmpeg -i $IN_FILE -vf scale=iw/$VAL:ih/$VAL,scale=$VAL*iw:$VAL*ih:flags=neighbor $OUT_FILE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment