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>.
.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; thetype()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()andtype(), can decode AVIF, and downloadshero.avifonly, 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 nottype(), 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.jpgagain.
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:
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.avifshould appear in the list andhero.webpandhero.jpgshould not. Ifhero.jpgappears instead, the second line is not being understood: check that every candidate has atype(), that the values are exact (image/avif, notimage/avi), and that theimage-set()line comes after theurl()line.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".