Responsive images with srcset and sizes, by example
Learn srcset and sizes with real numbers: how many pixels a hero needs on an iPhone 17 Pro, Galaxy S25 or MacBook Air, plus picture, lazy loading and Next.js.
srcset gives the browser a list of image files and their widths; sizes tells it how wide the image will be displayed. The browser multiplies that display width by the screen's device pixel ratio and downloads a file that's big enough, so a full-width hero gets about a 1206px file on an iPhone 17 Pro and a 2940px file on a MacBook Air 13″, instead of one giant file for everyone.
Here's how it works, with real devices and numbers you can copy.
The problem srcset solves
With a plain <img src="hero.jpg">, you have to pick one size for everyone.
- Pick 2940px wide and a phone on mobile data downloads a file several times bigger than it can show.
- Pick 800px wide and the image looks soft on every modern phone.
You can't fix this in CSS alone, because browsers start downloading images before they've finished reading your CSS. At that point the browser doesn't know how big the image will be on screen. sizes is you telling it in advance.
How the browser picks: display width × DPR
The browser needs display width in CSS pixels × device pixel ratio image pixels. (If DPR is new to you, read Device pixel ratio explained first.)
For a full-width hero, with the window maximized on desktop:
| Device | CSS width | DPR | Image pixels needed |
|---|---|---|---|
| iPhone SE (2nd & 3rd gen) | 375 | 2 | 750 |
| Galaxy S25 | 360 | 3 | 1080 |
| Galaxy A56 / A36 | 384 | 2.8125 | 1080 |
| Pixel 10 | 412 | 2.625 | ~1082 |
| iPhone 17 Pro | 402 | 3 | 1206 |
| iPhone 17 Pro Max | 440 | 3 | 1320 |
| iPad Air 11″ | 820 | 2 | 1640 |
| 15.6″ Full HD laptop @ 125% | 1536 | 1.25 | 1920 |
| 27″ QHD monitor | 2560 | 1 | 2560 |
| MacBook Air 13″ | 1470 | 2 | 2940 |
Two things to notice. First, most phones need 1080 to 1320 pixels for a full-width image, which is more than a lot of people expect. Second, the MacBook Air wants the biggest file of all, even though its viewport is narrower than the QHD monitor's, because it has twice the DPR.
The browser then looks at your list and usually takes the smallest file that covers what it needs. The spec leaves the final choice to the browser, so it may pick differently, for example if a larger version is already cached.
srcset with w descriptors, plus sizes
For an image that changes size with the layout, list each file with its real pixel width followed by w:
<img
src="/img/hero-1920.jpg"
srcset="
/img/hero-750.jpg 750w,
/img/hero-1080.jpg 1080w,
/img/hero-1320.jpg 1320w,
/img/hero-1640.jpg 1640w,
/img/hero-1920.jpg 1920w,
/img/hero-2940.jpg 2940w"
sizes="100vw"
width="1920"
height="1080"
alt="Team working around a laptop">
Walk it through with the table above:
- Galaxy S25 needs 1080 → takes
hero-1080.jpg. - iPhone 17 Pro needs 1206 → takes
hero-1320.jpg. - iPad Air 11″ needs 1640 → takes
hero-1640.jpg. - MacBook Air 13″ needs 2940 → takes
hero-2940.jpg. - Full HD laptop at 125% needs 1920 → takes
hero-1920.jpg.
src is the fallback for very old browsers; modern ones use srcset.
The widths in that list weren't picked at random. They line up with where real devices cluster. You can see every device's viewport and DPR on the devices page, including the MacBook and iPad lists.
sizes when the image isn't full width
Full-width heroes are the easy case. Now take a blog card grid: one column on phones, two columns from 640px, three columns of 384px from 1024px.
<img
src="/img/card-800.jpg"
srcset="
/img/card-400.jpg 400w,
/img/card-800.jpg 800w,
/img/card-1080.jpg 1080w,
/img/card-1320.jpg 1320w"
sizes="(min-width: 1024px) 384px, (min-width: 640px) 50vw, 100vw"
width="1320"
height="743"
alt="Screenshot of the dashboard"
loading="lazy">
How to read sizes: the browser goes left to right and uses the first condition that matches. The last value has no condition; it's the default. So order matters: put the widest condition first.
Now the maths on real devices:
| Device | Matching rule | Display width | × DPR | File chosen |
|---|---|---|---|---|
| MacBook Air 13″ (1470) | min-width: 1024px |
384 | × 2 = 768 | 800w |
| iPad Air 11″ (820) | min-width: 640px |
410 | × 2 = 820 | 1080w |
| Galaxy S25 (360) | default | 360 | × 3 = 1080 | 1080w |
| iPhone 17 Pro (402) | default | 402 | × 3 = 1206 | 1320w |
Yes: the phone downloads a bigger card image than the laptop. That's correct, because on the phone the card fills the screen at DPR 3.
sizes doesn't have to be pixel-perfect. Ignoring padding and gaps is fine; being a little generous is better than being stingy. The two classic mistakes are:
- Leaving
sizesout. The browser assumes100vw, so for a 384px card on a MacBook Air it thinks it needs 2940 pixels and grabs your biggest file. - Copying
sizes="33vw"everywhere. On a phone where the card is actually full width, the browser picks a file a third of the size it needs, and it looks blurry.
If you use Tailwind, the conditions line up with its breakpoints (sm 640, md 768, lg 1024, xl 1280). Tailwind breakpoints on real devices shows which devices fall where.
x descriptors for fixed-size images
If an image is always the same CSS size, like a 160×40 logo or a 48×48 avatar, you don't need sizes. Use x descriptors, which map files straight to DPR:
<img
src="/logo-160.png"
srcset="/logo-160.png 1x, /logo-320.png 2x, /logo-480.png 3x"
width="160"
height="40"
alt="Acme">
A DPR 1 monitor gets the 160px file, a MacBook gets 320, an iPhone gets 480. Android phones with in-between ratios like 2.625 get whichever the browser thinks fits best. For logos and icons, SVG is even simpler: one file, sharp everywhere.
<picture> for art direction
Sometimes a wide hero crop makes no sense on a tall phone screen: the subject ends up as a speck in the middle. <picture> lets you swap in a different crop:
<picture>
<source
media="(max-width: 767px)"
srcset="/img/hero-tall-750.jpg 750w, /img/hero-tall-1080.jpg 1080w, /img/hero-tall-1320.jpg 1320w"
sizes="100vw">
<img
class="hero-img"
src="/img/hero-wide-1920.jpg"
srcset="/img/hero-wide-1080.jpg 1080w, /img/hero-wide-1920.jpg 1920w, /img/hero-wide-2940.jpg 2940w"
sizes="100vw"
width="1920"
height="800"
alt="Our studio in Lisbon">
</picture>
The browser uses the first <source> whose media matches, and falls back to the <img>. The <img> is required, and it's where alt, width, height and loading go. Because the two crops have different shapes, set the shape per breakpoint in CSS:
.hero-img {
width: 100%;
height: auto;
aspect-ratio: 1920 / 800;
object-fit: cover;
}
@media (max-width: 767px) {
.hero-img { aspect-ratio: 3 / 4; }
}
<picture> also handles formats: add <source type="image/avif" srcset="..."> before the <img> and browsers that support AVIF take it, while the rest fall back to your JPEG. MDN's responsive images guide covers both uses.
width and height stop layout shift
Always put width and height attributes on your <img>, set to the image's real pixel size (or any numbers with the same ratio). With this in your CSS:
img {
max-width: 100%;
height: auto;
}
the browser uses those two numbers to reserve the right amount of space before the file arrives. Without them the page jumps when images load, which is annoying for readers and counts against you in Core Web Vitals (web.dev on CLS).
Lazy loading, and when not to
loading="lazy" tells the browser not to fetch an image until the reader scrolls near it. Use it on everything below the first screen: cards, gallery images, screenshots further down.
Don't use it on your hero or anything visible on first load. That image is usually your Largest Contentful Paint, and lazy loading it delays it. For the hero, do the opposite:
<img src="/img/hero-1920.jpg" srcset="..." sizes="100vw"
width="1920" height="1080" alt="..." fetchpriority="high">
Where "first load" ends depends on the screen: on a 402×874 iPhone with Safari's bars showing, only about 718px of page is visible at first, so fewer images count as above the fold than on a laptop.
Does your site fit?
Paste your address (or localhost:3000). doesitfit shows it on real screen sizes at once and tells you, bluntly, where it spills.
Free, no sign-up. Or ask Claude, ChatGPT or Codex to check it.
How Next.js <Image> maps to this
Next.js generates srcset for you, but it still needs sizes from you (Next.js Image docs):
- Without
sizes, it creates a small1x, 2xsrcset based on thewidthprop. Perfect for fixed-size images like logos and avatars. - With
sizes, it creates a fullwsrcset from itsdeviceSizeslist (640, 750, 828, 1080, 1200, 1920, 2048, 3840 by default) plusimageSizes, and passes yoursizesstraight through. - With
fill, always addsizes, or the browser assumes the image is100vw. - Images are lazy-loaded by default. For your hero, the docs recommend
loading="eager"orfetchPriority="high"; setting both is the safe choice.
A hero and a card look like this:
import Image from 'next/image'
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Team working around a laptop"
width={1920}
height={1080}
sizes="100vw"
loading="eager"
fetchPriority="high"
style={{ width: '100%', height: 'auto' }}
/>
)
}
export function Card({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={1320}
height={743}
sizes="(min-width: 1024px) 384px, (min-width: 640px) 50vw, 100vw"
/>
)
}
With the default list, an iPhone 17 Pro (needing 1206) sits between the 1200 and 1920 files, and a Galaxy S25 (needing 1080) lands exactly on 1080. You can change deviceSizes in next.config if your traffic leans heavily one way.
The takeaway
- Work out the display width of each image in CSS pixels at each breakpoint.
- Multiply by DPR: roughly 1080 to 1320 for full-width images on phones, 1640 for an iPad, up to 2940 for a MacBook Air.
- Use
wdescriptors +sizesfor anything that resizes,xdescriptors for fixed-size images,<picture>for different crops or formats. - Always set
widthandheight, lazy-load below the fold, and give the herofetchpriority="high". - Check your layout on real viewports, because the right
sizesdepends on where your breakpoints actually land. Responsive breakpoints in 2026 is a good next read.
Questions people ask
What happens if I leave out sizes?
If your srcset uses w descriptors and there is no sizes attribute, the browser assumes the image is as wide as the viewport (100vw). For images that are actually smaller, like cards or thumbnails, that means downloading far bigger files than needed.
Should I use srcset or the picture element?
Use srcset on a plain img when you have the same image at different sizes. Use picture when you need a different crop on different screens, or want to offer newer formats like AVIF with a fallback.
How many image widths should I generate?
Four to six widths is plenty for most sites. For a full-width image, something like 750, 1080, 1320, 1640, 1920 and 2940 covers the real phones, tablets and laptops in use today.
Does srcset work for CSS background images?
No, srcset is only for img and picture. For backgrounds, use the CSS image-set() function, which lets the browser choose between 1x, 2x and 3x files.