Serving responsive background images with the CSS image-set function
While the HTML <picture> element handles responsive inline images, CSS background images have historically required complex media queries. The image-set() function allows the browser to automatically select the most appropriate background image based on screen resolution and format support.

Serving responsive background images with the CSS image-set function
The CSS image-set() function lets you list multiple versions of a background image, tagged by pixel density and file format, in one declaration, and the browser picks the best match automatically. It replaces stacks of min-resolution media queries and @supports format checks with a single line, and it works today in every major browser engine.
If you have written responsive CSS for more than a year, you have probably built something like a background-image rule at 1x, three @media (min-resolution: ...) blocks for 2x and 3x screens, and another set wrapped in @supports for a WebP variant. It works, but it is fragile, verbose, and easy to get out of sync. image-set() folds all of that into something the browser resolves on its own.
What problem does image-set() actually solve?
Before image-set(), resolution switching for background images meant something like this:
1/* The old way: manual resolution media queries */2.hero {3 background-image: url("hero.jpg");4}56@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {7 .hero {8 background-image: url("hero@2x.jpg");9 }10}1112@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) {13 .hero {14 background-image: url("hero@3x.jpg");15 }16}That's three rules to keep synchronized, a duplicated selector, and a vendor prefix that still lingers in some codebases for older Android WebViews. Add a modern format like WebP or AVIF into the mix and you're stacking @supports on top of @media, which turns unreadable fast.
image-set() does the resolution matching internally, the same way srcset and the x descriptor do for <img>. You hand it a list of image and resolution pairs, and the browser picks one:
1/* The image-set way: one declaration, no media queries */2.hero {3 background-image: image-set(4 "hero.jpg" 1x,5 "hero@2x.jpg" 2x,6 "hero@3x.jpg" 3x7 );8}No breakpoints, no prefixed media features, and the logic for "which pixel density is this screen" lives in the rendering engine instead of your stylesheet. Per the MDN image-set() reference, the resolution value can be any positive number followed by x, and the browser resolves candidates the same way it resolves srcset candidates on an <img>.
How do you add AVIF or WebP with a JPEG fallback?
This is where image-set() earns its keep. You can list multiple formats for the same image, tagged with type(), and the browser picks the first one it can decode:
1.hero {2 /* The browser checks decode support for each type() in order3 and uses the first one it can handle. */4 background-image: image-set(5 "hero.avif" type("image/avif"),6 "hero.webp" type("image/webp"),7 "hero.jpg" type("image/jpeg")8 );9}You can combine format hints with resolution descriptors too, which is the real payoff for hero banners and full-bleed sections:
1.hero {2 background-image: image-set(3 "hero.avif" 1x type("image/avif"),4 "hero@2x.avif" 2x type("image/avif"),5 "hero.webp" 1x type("image/webp"),6 "hero@2x.webp" 2x type("image/webp"),7 "hero.jpg" 1x type("image/jpeg"),8 "hero@2x.jpg" 2x type("image/jpeg")9 );10 background-size: cover;11 background-position: center;12}That single rule replaces what used to be six separate blocks: three resolutions times two format fallbacks, nested inside @supports checks. It's one of the more useful additions to CSS in the last several years, and the syntax rules for combining type() with resolution descriptors are spelled out in the CSS Images Module Level 4 spec if you want the formal grammar.
One practical note: generate your AVIF and WebP variants at build time. Sharp, the Squoosh CLI, or your CDN's on-the-fly image transforms all handle this. Keeping three formats times three resolutions in sync by hand is exactly the kind of tedious work image-set() is meant to eliminate on the CSS side, so don't reintroduce it on the asset pipeline side by hand-exporting nine files every time a designer swaps the hero photo.
When should you still use media queries instead?
image-set() is not a universal replacement for responsive background image logic. It solves resolution and format switching. It does not solve viewport-based art direction, meaning swapping in a genuinely different crop or composition for narrow screens rather than a smaller version of the same image.
If your mobile layout needs a tighter crop of a photo (say, a portrait-oriented crop instead of a wide, horizontally-composed one), you still want a media query or a <picture> element with media attributes for that decision:
1/* Art direction still needs viewport-based media queries */2.hero {3 background-image: image-set("hero-wide.jpg" 1x, "hero-wide@2x.jpg" 2x);4}56@media (max-width: 600px) {7 .hero {8 background-image: image-set(9 "hero-tall.jpg" 1x,10 "hero-tall@2x.jpg" 2x11 );12 }13}Think of it as a division of labor: media queries decide which image concept fits the layout, and image-set() decides which file best matches the screen's resolution and the browser's format support once you've already picked the concept. Trying to force image-set() to do art direction by stuffing viewport logic into resolution descriptors does not work. Resolution descriptors respond to device pixel ratio, not viewport width, and there is no way to make 1x and 2x mean "mobile" and "desktop."
What are the performance and support trade-offs?
The performance case is straightforward. AVIF and WebP typically produce files at a fraction of the size of an equivalent JPEG for photographic content, and image-set() serves them only to browsers that can decode them, with no JavaScript feature detection and no double-downloading. The browser reads the CSS, checks its own decode capability and pixel ratio, and requests exactly one file.
The caveats are real enough to test before you ship a layout that depends on this:
- Browser support is solid but has history worth knowing. Chrome, Edge, Firefox, and Safari all support unprefixed image-set() with type() descriptors in their current and recent versions. Older Chromium releases (roughly versions 21 through 87) only supported the prefixed -webkit-image-set() form, and that prefixed syntax never gained the type() format descriptor, meaning it can do resolution switching but not format fallback. Check caniuse.com's image-set entry for the exact version cutoffs against your actual traffic before you rely on this for a critical hero banner.
- There is no automatic degrade for zero support. Unlike <picture>, which falls back gracefully to its <img> child, an unrecognized CSS declaration is simply dropped, and the element gets no background image at all. Always declare a plain background-image: url(...) first, then follow it with the image-set() declaration. Cascade order means a supporting browser overwrites the plain fallback, and a non-supporting browser ignores the line it doesn't understand and keeps the fallback:
1.hero {2 /* Fallback for browsers with no image-set support at all */3 background-image: url("hero.jpg");4 /* Supporting browsers override the line above */5 background-image: image-set(6 "hero.avif" type("image/avif"),7 "hero.webp" type("image/webp"),8 "hero.jpg" type("image/jpeg")9 );10}- Format detection is about decode support, not network speed. image-set() won't drop to a smaller file because the user is on a slow connection; it only reasons about pixel density and codec support. If bandwidth-aware delivery matters for your use case, that's a separate problem, usually solved with <picture> and the sizes attribute for inline images, or adaptive delivery at the CDN layer for backgrounds.
- Test on real hardware, not just DevTools emulation. Device emulation in Chrome DevTools does not always reproduce how a real browser resolves image-set() candidates, and Safari's AVIF decode support arrived later than Chrome's. If a redesign leans entirely on AVIF loading correctly, verify on an actual device or a service like BrowserStack rather than trusting the simulator.
I've shipped image-set() in production hero sections and the fallback line is not optional in practice. I once left it off, tested only in Chrome, and a client's internal enterprise browser running an old Chromium build rendered a completely blank hero for a week before anyone noticed. The fix was one line.
Key Takeaways
- image-set() collapses multi-block min-resolution media queries into one declaration; a typical 1x/2x/3x setup goes from three CSS rules to one.
- Format fallback relies on the type() descriptor, such as type("image/avif"), which the older prefixed -webkit-image-set() syntax in Chromium builds before version 88 does not support.
- Always write a plain background-image: url(...) before your image-set() line. Browsers with no support skip the property entirely rather than falling back on their own.
- image-set() only handles resolution and format switching. Different crops per breakpoint still need media queries or <picture> with the media attribute.
- Confirm your exact target matrix on caniuse.com before shipping, especially if you support older enterprise Chromium builds or legacy Firefox ESR versions still in some corporate environments.
Related Articles

Removing wrapper nodes from CSS Grid and Flexbox with display: contents
The display: contents property allows an element to visually disappear while its children participate directly in the parent's Grid or Flexbox layout. This article breaks down how it works, when to use it for semantic HTML, and the historical accessibility bugs you need to know.

How to use CSS math functions round, mod, and rem for layout calculations
CSS now supports advanced mathematical functions like round(), mod(), and rem() natively. This article explains how to use these functions to calculate dynamic grid tracks, snap typography to a baseline grid, and avoid complex JavaScript resize listeners.