Web Performance · Guide

How Do I Give a CSS Background Image a WebP or AVIF Fallback?

10 min read · October 15, 2026 · Mochify Engineering Team

Give a CSS background image a WebP or AVIF fallback with two background-image lines in the same rule: a plain url() pointing at the JPEG or PNG, then the CSS image-set() function listing the AVIF, the WebP and the JPEG, each tagged with type() so the browser takes the first format it can open. image-set() with type() has been Baseline, meaning it works in Chrome, Edge, Firefox and Safari, since September 2023; any browser older than that ignores the second line and paints the first. The <picture> element cannot do this job, because a background is set by a stylesheet, not by an <img>.

styles.css
.hero {
  background-image: url("hero.jpg");
  background-image: image-set(
    url("hero.avif") type("image/avif"),
    url("hero.webp") type("image/webp"),
    url("hero.jpg")  type("image/jpeg")
  );
}

If you are here, you probably met one of two things. A speed test (PageSpeed Insights, Lighthouse, GTmetrix) flagged "Serve images in modern formats" on a page whose largest image is a section background, and the <picture> fix every tutorial gives turned out not to apply, because <picture> only replaces <img> tags. Or you swapped the background for a .webp and someone on an older iPhone sent you a screenshot of a blank gray band where the photo should be. Themes and site builders leave this unsolved because the background line in their stylesheet names one file, and the settings and plugins that convert a page's images work on <img> tags. Taking control of it is an edge, and it is not a chore: the whole fix is the two lines above and two extra files beside the JPEG you already have.

Published October 15, 2026 by the Mochify Engineering Team. The stylesheet half of the picture-element series: two lines of CSS, the browsers that ignore them, and the checks, dated October 2026.

What a background image is, and three terms this answer uses

A web page has two kinds of picture. A content image is part of the page itself: it is placed with an <img> tag, it sits in the flow of the text, it has a description for screen readers and search engines, and it is what the <picture> element controls. A background image is paint. The page's stylesheet (CSS, the text file that tells a browser how the page should look) says "fill this section with hero.jpg", the browser paints the file behind whatever is in that section, and the headline and buttons sit on top of it. The photo behind a hero heading, the texture behind a pricing table and the picture on a card that still shows text are usually backgrounds. A background is not part of the content: it has no description, search engines do not index it, and a visitor cannot right-click and save it.

That last point is also the test. In Chrome or Edge, right-click the picture: if the menu offers Save image as or Open image in new tab, it is a content image and the <picture> element is the fix. If the menu shows only the page options (Back, Reload, Inspect and so on), it is a background, and this answer is the one you want. Three more terms cover the rest:

  • Declaration: one instruction in a stylesheet, such as background-image: url("hero.jpg"), which means "paint hero.jpg behind this element". If a browser does not understand a declaration, it throws that one line away and keeps the rest of the rule.

  • image-set(): a way of writing a background as a list of candidate files instead of one file, so the browser can choose. It was built for sending sharper images to high-resolution screens; the type() part is what turns it into a format fallback.

  • type(): the label on each candidate naming its format, written as a MIME type: image/avif, image/webp, image/jpeg, image/png. The browser skips any candidate whose type it cannot decode and uses the first one it can.

How image-set() picks a file: three visitors through the two lines

Start with the version you already have, a single declaration: background-image: url("hero.jpg"). Every browser understands it, and every visitor downloads the JPEG. The fix keeps that line and adds the image-set() line after it. When a rule sets the same property twice, the browser applies the last declaration it understands, so the second line wins wherever it is understood and the first line covers everyone else. What the rule above gives three real visitors:

  • Someone on a current Chrome, Firefox, Edge or Safari, including any iPhone updated since September 2023. Their browser understands image-set() and type(), can decode AVIF, and downloads hero.avif only, typically a fraction of the JPEG's size for a photograph. The WebP and the JPEG are never requested.

  • Someone on Safari 14, 15 or 16, say an iPhone that was never updated past iOS 16. Those versions understand image-set() but not type(), so they cannot use the second line and fall back to the first: hero.jpg, exactly what they got before. Safari 16 could have opened the WebP, so this visitor pays for the fallback with a larger file, but nothing is broken and nothing is blank.

  • Someone on a browser that has never heard of image-set(), such as an old desktop Chrome or the built-in browser of a TV. The second line is invalid to them, so it is discarded and the first line applies: hero.jpg again.

One download per visitor, and the JPEG is the safety net for everyone the new line does not reach.

The common broken version. The most-copied snippets have only the image-set() line, or put the WebP in the plain url() line on the grounds that WebP works everywhere now, which turns the WebP fallback into the thing that needs a fallback. Either way, the Safari 15 visitor above gets a file their browser cannot open, or no file at all, and the section is a blank band. The fix is the first line, and it must be the JPEG or PNG. The other broken version is the two lines in the wrong order: put the url() line second and it is the last declaration every browser understands, so every visitor gets the JPEG and the speed test still fails. Plain url() first, image-set() second.

image-set() browser support: which versions ignore type()

image-set() with type() is listed by MDN as Baseline Widely available, "available across browsers since September 2023", the month Safari 17 shipped it: Chrome 113, Edge 113, Firefox 89 and Safari 17 all honor the full form. The versions that understand image-set() but not type() are Safari and iOS Safari 14 through 16 and Firefox 88; MDN's compatibility data and caniuse agree on that list. Chrome 20 to 112 and Edge 79 to 112 recognized only a prefixed -webkit-image-set() with plain url() candidates, so the unprefixed line is invalid to them and the plain line applies. Some AI answers add a -webkit-image-set() line as well; we leave it out, because neither MDN nor caniuse records type() working in the prefixed form, and those browsers are already served by the plain line. Across all browsers in use, caniuse puts image-set() at 97.27% of global traffic (94.13% full support plus 3.14% partial). The first line exists for the rest, and for the partial band.

How to check which file a visitor gets

Two checks, no coding needed, in Chrome, Edge or Firefox on your own computer:

  1. See the AVIF being chosen. Open the page, right-click anywhere on it, choose Inspect, and click the Network tab in the panel that opens. Click the Img filter, then reload the page (Ctrl+R, or Cmd+R on a Mac). hero.avif should appear in the list and hero.webp and hero.jpg should not. If hero.jpg appears instead, the second line is not being understood: check that every candidate has a type(), that the values are exact (image/avif, not image/avi), and that the image-set() line comes after the url() line.

  2. See what an old browser gets, without owning an old phone. In your stylesheet, temporarily disable the image-set() line by putting /* before it and */ after its closing );, save, and reload. The section should show the JPEG exactly as before. That is what the Safari 15 visitor and the TV visitor see. Then put the line back.

@supports, detection scripts and the server route

Three other answers circulate. One is wrong, and two are worth knowing about.

@supports (background-image: url("hero.webp")) does not test what it looks like it tests. The @supports rule "checks if a browser supports the specified <property>: <value> declaration" (MDN), and the CSS standard defines support as the browser accepting that declaration rather than rejecting it as a parse error (CSS Conditional Rules Level 3, section 6.1). background-image: url("hero.webp") is a perfectly good declaration to a browser that cannot open WebP, because the test is about the line of CSS, not the file it names. So the WebP rule inside that @supports block is applied on browsers that cannot show it, which is the blank band again. type() inside image-set() is the format test; @supports is not.

Detection scripts (Modernizr and its descendants) do work. A script tests whether the browser can decode a tiny WebP, adds a class such as webp to the page, and your stylesheet carries a .webp .hero rule next to the plain one. It was the answer before 2023. The costs are a script that has to run before the background can switch, and two rules per image. Two lines of CSS now do the same job with no script.

The server route needs no stylesheet change at all. An image CDN, or a server configured for it, reads the Accept header a browser sends with each image request, which lists the formats that browser can open, and returns an AVIF or WebP under the JPEG's own address. If your host or CDN offers this, turn it on and the background line can stay as it is; it is a setting on their side, so their documentation is the place to check.

When you do not need this

  • The background is a flat color, a gradient or a small repeating pattern. The file is already tiny, and converting it saves bytes nobody will notice. Spend the effort on the large photographic backgrounds: hero sections and full-width banners.

  • Your CDN already negotiates formats. Then the stylesheet is not where the saving happens; see the server route above.

  • The image is content, not decoration. Google does not index CSS background images, so a product photo, a portrait, or anything you would want to appear in image search belongs in an <img> inside a <picture> element, not in a background; our guide to the HTML <picture> tag covers that side, and it is the only place this answer sends you for markup. For a true background, the fallback is about what the last 3 in 100 visitors see, not about search.

Making the AVIF and WebP files

The rule needs two extra files beside the JPEG, and the quickest way to get them is to describe the result. Open Magic Flow, drop hero.jpg, and type convert to avif; download the result, drop the JPEG again and type convert to webp. If the section is 1600 pixels wide, say so (convert to avif, 1600px wide) and the file comes back resized as well as re-encoded, which is often the bigger saving. For a flat-color pattern or a logo-like background, ask for convert to webp, lossless and keep a PNG as the plain-url() fallback; AVIF has no lossless path. From a terminal, the mochify CLI does the same for a folder: mochify hero.jpg -t avif and mochify hero.jpg -t webp. Either way the image travels to api.mochify.app, is encoded in memory and is wiped the moment the encode finishes. Twenty-five images a month are free with an account, three without one.

Make the two files for your first background now at mochify.app/flow: drop the JPEG and type "convert to avif", then "convert to webp".