{# Blog title goes here #}

CSS desaturated background image

When I started this blog I thought it'd be fun to have an image as the header's background. This led me of course to spending way too much time hunting for the perfect background image (much more fun than writing actual articles, isn't it?). I quickly realized that using a plain image or photo wasn't going to work so well since it makes the text on top of the image hard to read a lot of the times.

The solution I found was to desaturate the image, but because I'm not very good with image manipulation software I thought I would try and find a way to do that in pure CSS. This would also let me experiment with trying to find the right desaturation amount.

A quick search later (my browser history reveals I used the search prompt "css transparency effect background image") I ended up on a promising stackoverflow answer.

The idea is to create a stack of two background "images":

header {
  background:
    /* 1. a semi-transparent overlay */,
    /* 2. the actual image we want to use */;
}

The clever bit of this solution is the overlay "image" which is created in pure CSS by (ab)using the linear-gradient function. Normally you use linear-gradient(color1, color2) to create a background where the color smoothly shifts from color1 to color2 (like this one going from red to yellow:    ). By using linear-gradient(color1, color1) we get a background in a single color (because it shifts from itself to itself), and that background will scale to whatever dimensions necessary. The final piece of the puzzle is that we can use a transparent color, using the #RRGGBBAA notation for example.

Live demo

Here's how it looks like in practice (you can adjust the transparency using the slider and pick a different base color):

Sample text

%
header {
  background:
    linear-gradient(, ),
    url("/path/to/background.jpeg");
}

Further improvements

CSS variable

One easy win is to use a CSS variable to avoid having to declare the color twice:

header {
  --overlay: ;

  background:
    linear-gradient(var(--overlay), var(--overlay)),
    url("/path/to/background.jpeg");
}

Light/Dark mode

Now that we have a CSS variable, it's trivial to add support for light/dark mode:

header {
  /* light mode by default */
  --overlay: ;

  background:
    linear-gradient(var(--overlay), var(--overlay)),
    url("/path/to/background.jpeg");
}

@media only screen and (prefers-color-scheme: dark) {
  /* dark mode variant: only the color needs to be redefined */
  --overlay: #13171fa0;
}
header {
  /* dark mode by default */
  --overlay: ;

  background:
    linear-gradient(var(--overlay), var(--overlay)),
    url("/path/to/background.jpeg");
}

@media only screen and (prefers-color-scheme: light) {
  /* light mode variant: only the color needs to be redefined */
  --overlay: #ffffffcc;
}

image-set()

One last improvement I made on the site was to use the image-set() function to provide browsers that support it with alternative image formats. image-set() is quite powerful and can serve different files based on screen resolution, screen density, or image format support. I decided to only use the different image formats since it was easier for me to manage (and I picked the formats avif, webp and jpeg because that seemed like a good idea):

header {
  --overlay: ;

  /* fallback for browsers that don't support image-set() */
  background:
    linear-gradient(var(--overlay), var(--overlay)),
    url("/path/to/background.jpeg");

  /* firefox currently only supports the non-standard -webkit-image-set() */
  background:
    linear-gradient(var(--overlay), var(--overlay)),
    -webkit-image-set(
      url("/path/to/background.avif") type("image/avif"),
      url("/path/to/background.webp") type("image/webp"),
      url("/path/to/background.jpeg") type("image/jpeg"),
    );

  /* standard image-set() for everyone else (aka. webkit 😿) */
  background:
    linear-gradient(var(--overlay), var(--overlay)),
    image-set(
      url("/path/to/background.avif") type("image/avif"),
      url("/path/to/background.webp") type("image/webp"),
      url("/path/to/background.jpeg") type("image/jpeg"),
    );
}