Use WebP Images in CSS: Backgrounds and image-set()

Serve WebP background images in CSS using image-set() for modern browsers, or JavaScript feature detection with a .webp class for broader compatibility.

CSS background images require a different approach to WebP delivery than the HTML <picture> element. Because CSS backgrounds live outside the document’s image pipeline, you have two main strategies available: the image-set() CSS function, which modern browsers handle natively, and JavaScript feature detection, which adds a class to <html> that your CSS can target for maximum browser coverage.

Using image-set() #

The image-set() CSS function lets you provide multiple image candidates at different formats or resolutions and lets the browser pick the best one. All major modern browsers support the standard type() syntax.

.hero {
  background-image: url('hero.jpg'); /* fallback for very old browsers */
  background-image: image-set(
    url('hero.webp') type('image/webp'),
    url('hero.jpg') type('image/jpeg')
  );
  background-size: cover;
  background-position: center;
}

The browser ignores image-set() entirely if it doesn’t understand the syntax and falls through to the plain url() on the line above — making the bare url() your safety net.

Use image-set() for all new projects. It is now supported across Chrome, Firefox, Safari, and Edge without any JavaScript dependency, keeping your feature-detection logic out of the critical rendering path.

Vendor-Prefixed Version for Older Browsers #

Older Chrome and Safari releases implemented -webkit-image-set() before the standard was finalised. If you need to support those browsers, stack all three declarations. Browsers apply the last one they understand.

.hero {
  background-image: url('hero.jpg');
  background-image: -webkit-image-set(
    url('hero.webp') 1x,
    url('hero.jpg') 1x
  );
  background-image: image-set(
    url('hero.webp') type('image/webp'),
    url('hero.jpg') type('image/jpeg')
  );
}

The older -webkit-image-set() syntax uses density descriptors (1x, 2x) rather than the type() function to describe format preference. This means it selects images based on device pixel ratio, not file type, so fallback behaviour differs slightly from the standard syntax. Prefer the standard image-set() with type() wherever possible.

JavaScript Feature Detection #

For full control over which browsers receive WebP backgrounds, detect WebP support in JavaScript and add a class to the <html> element. Your CSS can then target .webp and .no-webp selectors independently.

Detection Script #

Run this script as early as possible — ideally in a <script> tag in <head> before your stylesheet — to avoid a flash of the fallback image.

// Detect WebP support and add class to <html>
function setWebPClass() {
  const img = new Image();
  img.onload = () => document.documentElement.classList.add('webp');
  img.onerror = () => document.documentElement.classList.add('no-webp');
  img.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';
}
setWebPClass();

The data URI is a minimal valid WebP file. If the browser can decode it (onload fires), WebP is supported. If not (onerror fires), it isn’t.

CSS Using the Class #

/* Serve WebP to supporting browsers */
.webp .hero {
  background-image: url('hero.webp');
}

/* Serve JPEG to everything else */
.no-webp .hero {
  background-image: url('hero.jpg');
}

This approach works in every browser that supports JavaScript, which makes it the most compatible option when you need to cover legacy environments.

Using Modernizr #

If your project already includes Modernizr, it performs WebP detection automatically and exposes the result via Modernizr.webp. Modernizr also adds the .webp and .no-webp classes to <html> in the same pattern described above, so your CSS selectors work identically without writing a custom detection function.

// Modernizr async detection
Modernizr.on('webp', (result) => {
  if (result) {
    // WebP is supported
  } else {
    // WebP is not supported
  }
});

Choosing Between image-set() and JS Detection #

  1. New project with modern browser targets — Use image-set() with type(). No JavaScript required, works natively in all evergreen browsers, and keeps your CSS self-contained.
  2. Existing project with legacy browser requirements — Use the JavaScript feature-detection approach. It covers browsers that predate image-set() support and integrates cleanly with frameworks that already manipulate <html> classes.
  3. Project already using Modernizr — Rely on Modernizr.webp — it saves you from writing duplicate detection logic and its .webp/.no-webp class convention is already documented and understood by your team.

Combining Both Approaches #

You can layer image-set() as the primary rule and JS detection as a deeper fallback. Browsers that understand image-set() never evaluate the .no-webp rule, and browsers that don’t support image-set() fall through to whichever class was set by JavaScript.

.hero {
  background-image: url('hero.jpg'); /* deepest fallback */
}

.webp .hero {
  background-image: url('hero.webp'); /* JS detection */
}

/* Modern browsers override both with native image-set() */
.hero {
  background-image: image-set(
    url('hero.webp') type('image/webp'),
    url('hero.jpg') type('image/jpeg')
  );
}
Was this page helpful?