Filters, Blend Modes & Visual Effects

Reviewed & published by Brayan K

The CSS filter property applies graphic effects like blur(), brightness(), and grayscale() to an element, while mix-blend-mode controls how its colours blend with the layers beneath — together giving you Photoshop-style effects in pure CSS.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end of this lesson you'll be able to retouch images with pure CSS, build frosted-glass cards, tint photos with blend modes, and combine these into Instagram-style presets and Spotify-style duotones — no Photoshop, no image editing, just a few lines of CSS.

💡 Think of It Like This

Filters are the Instagram editing screen for a single photo. Slide "blur" up and the photo softens; slide "brightness" up and it glows. You're processing the picture itself, and you can stack several adjustments at once.

Blend modes are sheets of coloured acetate you lay over the photo — the colour mixes with what's underneath instead of just sitting on top. And backdrop-filter is frosted bathroom glass: the glass itself is clear, but everything behind it turns soft and dreamy. Same idea — three different things to blur: the element, the colours that meet it, or the world behind it.

1. The filter Property

The filter property applies graphical effects to an element — the same kinds of adjustments you'd reach for in a photo editor, but in CSS. It works on anything: images, text, a <div>, even a video. You give it one or more filter functions, and they run left to right like a pipeline.

FunctionTypical rangeEffect
blur(px)0 and upSoftens with a Gaussian blur
brightness(n)0 to ∞ (1 = normal)Lightens or darkens
contrast(n)0 to ∞ (1 = normal)Increases or flattens contrast
grayscale(%)0% to 100%Removes colour
hue-rotate(deg)0deg to 360degSpins every colour round the wheel
drop-shadow(...)x y blur colourShadow that follows the shape, not the box

Run the worked example. Each card applies one filter, and hovering removes it so you can compare against the untouched image. Read the comment beside every rule to see what value produces what.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS filters</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 20px; background: #0f172a; color: #e5e7eb; }
    .gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
    .card { background: #1e293b; border-radius: 12px; overflow: hidden; text-align: center; }
    .card img { width: 100%; height: 110px; object-fit: cover; transition: filter 0.3s; }
    .card p { margin: 0; padding: 6px; font-family: monospace; font-size: 12px; color: #94a3b8; }

    /* Each rule applies ONE filter; hovering resets it to compare. */
    .blur img       { filter: blur(3px); }                  /* soft, out-of-focus look */
    .blur img:hover { filter: blur(0); }                    /* back to sharp on hover */

    .bright img       { filter: brightness(1.5); }          /* 1.5 = 150% brightness */
    .bright img:hover { filter: brightness(1); }            /* 1 = normal */

    .contrast img       { filter: contrast(1.6); }          /* punchy, high contrast */
    .contrast img:hover { filter: contrast(1); }

    .gray img       { filter: grayscale(100%); }            /* fully black & white */
    .gray img:hover { filter: grayscale(0%); }              /* full colour again */

    .hue img       { filter: hue-rotate(120deg); }          /* shift every colour 120deg */
    .hue img:hover { filter: hue-rotate(0deg); }

    .shadow img       { filter: drop-shadow(4px 4px 6px rgba(0,0,0,0.6)); } /* shape-shadow */
    .shadow img:hover { filter: drop-shadow(0 0 0 transparent); }
  </style>
</head>
<body>
  <h2>Hover any image to see the original</h2>
  <div class="gallery">
    <div class="card blur"><img src="https://picsum.photos/300/200?random=1" alt="blur demo"><p>blur(3px)</p></div>
    <div class="card bright"><img src="https://picsum.photos/300/200?random=2" alt="brightness demo"><p>brightness(1.5)</p></div>
    <div class="card contrast"><img src="https://picsum.photos/300/200?random=3" alt="contrast demo"><p>contrast(1.6)</p></div>
    <div class="card gray"><img src="https://picsum.photos/300/200?random=4" alt="grayscale demo"><p>grayscale(100%)</p></div>
    <div class="card hue"><img src="https://picsum.photos/300/200?random=5" alt="hue-rotate demo"><p>hue-rotate(120deg)</p></div>
    <div class="card shadow"><img src="https://picsum.photos/300/200?random=6" alt="drop-shadow demo"><p>drop-shadow(...)</p></div>
  </div>

  <!-- Note: drop-shadow follows the image's real shape (e.g. a PNG cut-out),
       unlike box-shadow which always hugs the rectangular box. -->
  <!-- ✅ Expected result, measured in a real browser:
     .gallery -> display: grid
     .card -> overflow: hidden
     .card img -> object-fit: cover
     .card -> count: 6
  -->
</body>
</html>

2. Chaining Filters (and why order matters)

You can list several filter functions in one declaration, separated by spaces: filter: grayscale(40%) brightness(1.2) contrast(0.9);. That single line is how the "vintage", "dramatic", and "faded" looks of photo apps are built — a fixed recipe of small adjustments stacked together.

The functions run left to right, so order changes the result. brightness(1.5) blur(5px) brightens the sharp image first, then blurs the bright pixels; swap them and you blur first, then brighten the soft pixels — a different look. Treat the order as part of the effect.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Chained filters</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 20px; background: #0f172a; color: #e5e7eb; }
    .row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; }
    .preset { background: #1e293b; border-radius: 12px; overflow: hidden; text-align: center; }
    .preset img { width: 100%; height: 130px; object-fit: cover; transition: filter 0.4s; }
    .preset:hover img { filter: none; }                /* hover clears all filters */
    .preset p { margin: 0; padding: 6px; font-family: monospace; font-size: 11px; color: #94a3b8; }

    /* Each preset is a RECIPE: multiple functions chained, applied left to right. */
    .vintage img  { filter: sepia(60%) contrast(1.1) brightness(0.9); }
    .dramatic img { filter: contrast(1.4) brightness(0.8) saturate(1.5); }
    .faded img    { filter: grayscale(40%) brightness(1.2) contrast(0.85); }

    /* Same two filters, opposite order => visibly different results. */
    .ab1 img { filter: brightness(1.6) blur(4px); }   /* brighten THEN blur */
    .ab2 img { filter: blur(4px) brightness(1.6); }   /* blur THEN brighten */
  </style>
</head>
<body>
  <h2>Presets are just chained filters. Hover to reset.</h2>
  <div class="row">
    <div class="preset vintage"><img src="https://picsum.photos/300/200?random=11" alt="vintage preset"><p>sepia+contrast+brightness</p></div>
    <div class="preset dramatic"><img src="https://picsum.photos/300/200?random=12" alt="dramatic preset"><p>contrast+brightness+saturate</p></div>
    <div class="preset faded"><img src="https://picsum.photos/300/200?random=13" alt="faded preset"><p>grayscale+brightness+contrast</p></div>
    <div class="preset ab1"><img src="https://picsum.photos/300/200?random=14" alt="brighten then blur"><p>brightness THEN blur</p></div>
    <div class="preset ab2"><img src="https://picsum.photos/300/200?random=14" alt="blur then brighten"><p>blur THEN brightness</p></div>
  </div>

  <!-- Compare the last two cards (same photo): order alone changes the look. -->
  <!-- ✅ Expected result, measured in a real browser:
     .row -> display: grid
     .preset -> overflow: hidden
     .preset img -> object-fit: cover
     .preset -> count: 5
  -->
</body>
</html>

3. backdrop-filter — Frosted Glass

backdrop-filter blurs (or brightens, etc.) whatever is rendered behind an element, while the element's own content stays sharp. This is the "glassmorphism" look: a translucent card floating over a photo, with the photo blurred where it shows through.

Two things are required, and both are common stumbling blocks. First, the element needs a semi-transparent background like rgba(255,255,255,0.15) — a fully opaque background hides the blur completely. Second, Safari needs the -webkit-backdrop-filter prefix as well, so always write both.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>backdrop-filter</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; }
    .scene { position: relative; height: 360px; overflow: hidden; }
    .scene img { width: 100%; height: 100%; object-fit: cover; }   /* the background to blur */

    .glass {
      position: absolute; bottom: 24px; left: 24px; right: 24px;
      /* 1) SEMI-TRANSPARENT background so the blur behind it is visible. */
      background: rgba(255,255,255,0.15);
      /* 2) Blur what's BEHIND the card; saturate makes colours pop through. */
      backdrop-filter: blur(14px) saturate(1.6);
      -webkit-backdrop-filter: blur(14px) saturate(1.6);   /* Safari needs this */
      border: 1px solid rgba(255,255,255,0.25);
      border-radius: 16px; padding: 20px; color: white;
    }
    .glass h3 { margin: 0 0 8px; }
    .glass p  { margin: 0; opacity: 0.9; line-height: 1.5; }
  </style>
</head>
<body>
  <div class="scene">
    <img src="https://picsum.photos/1200/600?random=20" alt="background landscape">
    <div class="glass">
      <h3>Frosted glass card</h3>
      <p>The photo is blurred only where this translucent card overlaps it.
         The card's own text stays perfectly sharp.</p>
    </div>
  </div>

  <!-- Try it: change the card background to an opaque white (#ffffff) and the
       blur vanishes — there's nothing see-through for it to show in. -->
  <!-- ✅ Expected result, measured in a real browser:
     .scene -> position: relative
     .scene img -> object-fit: cover
     .glass -> position: absolute
     .glass p -> opacity: 0.9
  -->
</body>
</html>

4. Blend Modes — mix-blend-mode & background-blend-mode

A blend mode decides how the colours of two layers combine instead of one simply covering the other. mix-blend-mode blends an element with whatever sits behind it on the page — drop a coloured <div> over a photo with mix-blend-mode: multiply and the colour soaks into the photo.

background-blend-mode is the inward-looking cousin: it blends an element's own background layers together — say a background-image with a background-color — without touching anything else on the page. Worth knowing the names: multiply darkens (and makes white vanish), screen lightens, overlay boosts contrast, and color keeps the photo's brightness but takes the overlay's hue — perfect for tinting.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Blend modes</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 20px; background: #0f172a; color: #e5e7eb; }
    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; }

    .tile { position: relative; height: 130px; border-radius: 12px; overflow: hidden; }
    .tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
    /* This overlay is the SAME blue everywhere; only the blend mode changes. */
    .tile .overlay { position: absolute; inset: 0; background: #2563eb; }
    .tile span { position: absolute; bottom: 6px; left: 8px; z-index: 2; color: white;
                 font-family: monospace; font-size: 12px; text-shadow: 0 1px 3px #000; }

    .multiply .overlay { mix-blend-mode: multiply; }   /* darkens, blue soaks in */
    .screen   .overlay { mix-blend-mode: screen; }     /* lightens */
    .overlayb .overlay { mix-blend-mode: overlay; }    /* more contrast */
    .colorb   .overlay { mix-blend-mode: color; }      /* tints, keeps brightness */
    .darken   .overlay { mix-blend-mode: darken; }     /* keeps the darker pixel */
    .lighten  .overlay { mix-blend-mode: lighten; }    /* keeps the lighter pixel */

    /* background-blend-mode: blends THIS element's own layers (image + colour). */
    .selfblend { height: 130px; border-radius: 12px; margin-top: 16px;
      background-image: url('https://picsum.photos/600/300?random=29');
      background-size: cover; background-color: #db2777;
      background-blend-mode: multiply; }
  </style>
</head>
<body>
  <h2>Same blue overlay, six mix-blend-modes</h2>
  <div class="grid">
    <div class="tile multiply"><img src="https://picsum.photos/300/200?random=21" alt="multiply"><div class="overlay"></div><span>multiply</span></div>
    <div class="tile screen"><img src="https://picsum.photos/300/200?random=22" alt="screen"><div class="overlay"></div><span>screen</span></div>
    <div class="tile overlayb"><img src="https://picsum.photos/300/200?random=23" alt="overlay"><div class="overlay"></div><span>overlay</span></div>
    <div class="tile colorb"><img src="https://picsum.photos/300/200?random=24" alt="color"><div class="overlay"></div><span>color</span></div>
    <div class="tile darken"><img src="https://picsum.photos/300/200?random=25" alt="darken"><div class="overlay"></div><span>darken</span></div>
    <div class="tile lighten"><img src="https://picsum.photos/300/200?random=26" alt="lighten"><div class="overlay"></div><span>lighten</span></div>
  </div>

  <p style="margin-top:16px">background-blend-mode (one element blends its own image + pink colour):</p>
  <div class="selfblend"></div>
  <!-- ✅ Expected result, measured in a real browser:
     .grid -> display: grid
     .tile -> position: relative
     .tile img -> position: absolute
     .tile .overlay -> position: absolute
     .tile -> count: 6
  -->
</body>
</html>

🎯 Your Turn #1 — Grayscale-on-rest, colour-on-hover

A classic portfolio effect: thumbnails are black-and-white until you hover, when they bloom into colour. Fill in the blanks marked ___, then run it and check the expected result in the comments.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Your Turn 1</title>
  <style>
    /* 🎯 YOUR TURN — fill in the blanks marked ___ */

    body { font-family: system-ui, sans-serif; padding: 20px; background: #0f172a; }
    .thumb { width: 200px; border-radius: 12px; overflow: hidden; }
    .thumb img { width: 100%; height: 140px; object-fit: cover; transition: filter 0.3s; }

    /* 1) At rest, make the image fully black & white. */
    .thumb img { filter: ___; }            /* 👉 use  grayscale(100%) */

    /* 2) On hover, bring the colour fully back. */
    .thumb img:hover { filter: ___; }      /* 👉 use  grayscale(0%)   */
  </style>
</head>
<body>
  <div class="thumb">
    <img src="https://picsum.photos/300/200?random=40" alt="portfolio thumbnail">
  </div>

  <!-- ✅ Expected: the image is greyscale until you hover, then it turns full colour.
       Bonus: add  brightness(1.1)  to the hover filter to make it pop. -->
</body>
</html>

🎯 Your Turn #2 — Build a frosted-glass card

The card below sits over a photo but currently looks flat — the glass effect is missing. Add the semi-transparent background and the backdrop-filter (with its Safari prefix) to frost the photo behind it. Verify the expected look in the comments.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Your Turn 2</title>
  <style>
    /* 🎯 YOUR TURN — fill in the blanks marked ___ */

    body { font-family: system-ui, sans-serif; margin: 0; }
    .scene { position: relative; height: 320px; overflow: hidden; }
    .scene img { width: 100%; height: 100%; object-fit: cover; }

    .glass {
      position: absolute; bottom: 24px; left: 24px; right: 24px;
      border: 1px solid rgba(255,255,255,0.25);
      border-radius: 16px; padding: 20px; color: white;

      /* 1) Give the card a SEMI-TRANSPARENT white background (so blur shows). */
      background: ___;                  /* 👉 use  rgba(255,255,255,0.15) */

      /* 2) Blur what's behind the card. */
      ___                               /* 👉 add  backdrop-filter: blur(14px); */
      /* 3) Add the Safari-prefixed version too. */
      ___                               /* 👉 add  -webkit-backdrop-filter: blur(14px); */
    }
  </style>
</head>
<body>
  <div class="scene">
    <img src="https://picsum.photos/1000/500?random=41" alt="background photo">
    <div class="glass">
      <h3 style="margin:0 0 8px">Frosted card</h3>
      <p style="margin:0">The photo behind me should look blurred and frosted.</p>
    </div>
  </div>

  <!-- ✅ Expected: the photo is sharp everywhere except behind the card, where it's
       softly blurred — and the card's text stays crisp. If you see no blur, your
       background is probably opaque. -->
</body>
</html>

🧩 Mini-Challenge — Duotone effect from scratch

Support is faded now — only an outline is given. Build the Spotify-style duotone: a greyscaled photo with a coloured gradient blended on top in color mode. Lean on the worked examples in sections 1 and 4 if you get stuck.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Mini-Challenge</title>
  <style>
    /* 🧩 MINI-CHALLENGE: a two-tone "duotone" image
       1. Make a .duo container with position: relative, fixed size, overflow: hidden
       2. Put an <img> inside it and apply filter: grayscale(100%) contrast(1.1)
          so the photo is black & white
       3. Add a .duo::after pseudo-element that fully covers the container
          (position: absolute; inset: 0; content: '')
       4. Give ::after a linear-gradient background between two bold colours
          (e.g. blue -> pink)
       5. Blend that gradient into the photo with mix-blend-mode: color
          so it keeps the photo's brightness but takes the gradient's hue
       6. Remember pointer-events: none on ::after so it doesn't block clicks

       ✅ Expected: the photo looks like a single image painted in just two
          colours (a blue-to-pink duotone), not full colour and not plain grey. */

    /* your code here */
  </style>
</head>
<body>

  <!-- your markup here: a .duo container wrapping one <img> -->

</body>
</html>

⚠️ Common Errors (and the fix)

📋 Quick Reference

GoalUse this
Blur an elementfilter: blur(4px);
Black & whitefilter: grayscale(100%);
Lighten / darkenfilter: brightness(1.3);
Shift coloursfilter: hue-rotate(90deg);
Shape-following shadowfilter: drop-shadow(2px 2px 4px #000);
Chain several filtersfilter: grayscale(40%) contrast(1.2);
Frosted glassbackdrop-filter: blur(14px); (+ webkit)
Tint a photo with an overlaymix-blend-mode: multiply;
Blend an element's own layersbackground-blend-mode: multiply;

🎉 Lesson Complete

You can now retouch images and build rich visual effects with pure CSS. The essentials:

Practice quiz

What does the CSS filter property act on?

  • The element it is applied to (e.g. blurring that element)
  • Only the page background
  • Whatever is rendered behind a translucent element
  • Only sibling elements

Answer: The element it is applied to (e.g. blurring that element). filter processes the element itself — filter: blur(4px) blurs that element.

What does backdrop-filter act on?

  • The element's own text
  • Whatever is rendered behind the element, seen through its translucent background
  • The element's border only
  • The entire document at once

Answer: Whatever is rendered behind the element, seen through its translucent background. backdrop-filter blurs/adjusts what is behind a semi-transparent element — the glassmorphism effect.

When you chain filter functions, how are they applied?

  • All at once, order does not matter
  • Right to left
  • Left to right, like a pipeline, so order changes the result
  • In a random order

Answer: Left to right, like a pipeline, so order changes the result. Filters run left to right, so brightness(1.5) blur(5px) differs from blur(5px) brightness(1.5).

A common reason backdrop-filter shows no blur is that:

  • The element's own background is fully opaque, leaving nothing see-through
  • The element has too much text
  • filter was used instead of color
  • The page has no images

Answer: The element's own background is fully opaque, leaving nothing see-through. An opaque background hides the blur; you need a semi-transparent background like rgba(255,255,255,0.15).

Which prefixed property does Safari require alongside backdrop-filter?

  • -moz-backdrop-filter
  • -ms-backdrop-filter
  • -webkit-backdrop-filter
  • -o-backdrop-filter

Answer: -webkit-backdrop-filter. Safari needs -webkit-backdrop-filter with the same value as backdrop-filter.

What is the difference between mix-blend-mode and background-blend-mode?

  • mix-blend-mode blends an element with what is behind it on the page; background-blend-mode blends an element's own background layers
  • They are identical
  • mix-blend-mode only works on text; background-blend-mode only on images
  • background-blend-mode blends with other elements; mix-blend-mode blends background layers

Answer: mix-blend-mode blends an element with what is behind it on the page; background-blend-mode blends an element's own background layers. mix-blend-mode blends with the page beneath; background-blend-mode blends an element's own background layers.

With mix-blend-mode: multiply, what happens to white areas of the top layer?

  • They turn black
  • They stay unchanged and let the layer below show through (white vanishes)
  • They become fully opaque
  • They invert their color

Answer: They stay unchanged and let the layer below show through (white vanishes). Multiplying by white (1) leaves pixels unchanged, so white areas effectively disappear.

Which filter function produces a shadow that follows the shape of a PNG cut-out rather than its box?

  • box-shadow(...)
  • drop-shadow(...)
  • blur(...)
  • contrast(...)

Answer: drop-shadow(...). filter: drop-shadow() follows the element's actual shape, unlike box-shadow which hugs the rectangle.

What does filter: grayscale(100%) do?

  • Makes the element fully transparent
  • Removes all color, making the element fully black and white
  • Doubles the brightness
  • Rotates the hue by 100 degrees

Answer: Removes all color, making the element fully black and white. grayscale(100%) fully desaturates the element to black and white.

Why can adding a filter unexpectedly change how z-index resolves?

  • filter disables z-index entirely
  • Any filter value creates a new stacking context
  • filter forces position: absolute
  • filter only affects opacity

Answer: Any filter value creates a new stacking context. A non-none filter creates a new stacking context, which can re-order layered elements.

Continue this course

Frequently asked questions

What is the difference between filter and backdrop-filter?

They blur (or brighten, etc.) different things. filter processes the element it is applied to — put filter: blur(4px) on an image and the image goes blurry. backdrop-filter processes whatever sits behind a semi-transparent element — put it on a translucent card over a photo and the photo shows through frosted, while the card's own text stays sharp. Rule of thumb: filter = blur me; backdrop-filter = blur what's behind me.

Why is my backdrop-filter doing nothing?

Almost always one of three things. (1) The element's own background is fully opaque, so there is no see-through area for the blur to appear in — give it a semi-transparent background like rgba(255,255,255,0.15). (2) You are testing in Safari without the prefix — Safari needs -webkit-backdrop-filter as well as backdrop-filter. (3) Nothing is actually behind the element to blur. backdrop-filter only affects pixels rendered behind it, so it needs content underneath and a translucent foreground to reveal it.

Does the order of filter functions matter?

Yes. Filters are applied left to right, like a pipeline, so brightness(1.5) blur(5px) brightens the sharp image first and then blurs the bright result, while blur(5px) brightness(1.5) blurs first and then brightens the already-soft pixels. The two give visibly different looks. When you chain functions, treat the order as part of the effect, not an afterthought.

What is the difference between mix-blend-mode and background-blend-mode?

mix-blend-mode blends an element with whatever is rendered behind it on the page — one element against other elements. background-blend-mode blends an element's own background layers together (for example a background-image with a background-color, or two stacked images) without involving anything else on the page. Use mix-blend-mode to tint a photo with an overlay div; use background-blend-mode to merge a colour into a single element's background.

Are filters and blend modes bad for performance?

Most filters are cheap and run on the GPU, but blur — especially backdrop-filter: blur() spread over a large area — is the expensive one and can drop frame rates on phones. Keep blurred backdrops to small regions like cards and navbars rather than full-screen overlays, avoid animating large blurs, and test on a real mid-range device. A filter also creates a new stacking context, which can quietly change how z-index resolves, so check your layering after adding one.

Why does mix-blend-mode: multiply ignore my white text or white card?

That is how multiply works, not a bug. Multiply multiplies the colour channels, and anything multiplied by white (1) stays unchanged, while anything multiplied by black (0) becomes black. So with multiply, white areas vanish into the layer below and dark areas show through strongly — perfect for dropping a logo onto paper, but useless if you expected white to stay white. Pick screen or lighten instead when you want the light areas to survive.

Related lessons