CSS mix-blend-mode and background-blend-mode, with live examples

One CSS line mixes an element's colours with whatever sits behind it, the way layer modes work in an image editor. Here is what each mode does, and how to keep the effect where you want it.

mix-blend-mode tells the browser to mix an element's colours with whatever is painted behind it, instead of covering it. multiply darkens, screen lightens, and difference inverts. Its sibling background-blend-mode does the same with the background layers of a single element.

Pick a mode below. The white word and the three circles form one layer over a rainbow backdrop.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>mix-blend-mode picker</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  /* The backdrop: a white band on top, a black band at the bottom, colours in between */
  .stage {
    position: relative; height: 220px; border-radius: 12px; overflow: hidden;
    background:
      linear-gradient(#fff 0 22%, transparent 42% 58%, #000 78%),
      linear-gradient(90deg, #ff3b30, #ffcc00, #34c759, #00c7fb, #5856d6, #ff2d92);
  }
  /* The layer that blends with the stage. Its background stays transparent. */
  .layer { position: absolute; inset: 0; }
  .dot { position: absolute; width: 110px; height: 110px; border-radius: 50%; }
  .a { left: 6%;  top: 18px;  background: #ff7a00; }
  .b { left: 24%; top: 92px;  background: #0a84ff; }
  .c { right: 6%; top: 50px;  background: #808080; }
  .layer h2 {
    position: absolute; left: 0; right: 0; top: 64px; margin: 0; text-align: center;
    font: 900 clamp(48px, 16vw, 92px)/1 system-ui, sans-serif; color: #fff;
  }
  .modes { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 8px; }
  .modes button {
    font: 13px system-ui, sans-serif; padding: 6px 9px; border-radius: 8px;
    border: 1px solid #c9cdd4; background: #fff; cursor: pointer;
  }
  .modes button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  code { font: 600 14px ui-monospace, Consolas, monospace; }
  #note { margin: 4px 0 0; font-size: 14px; line-height: 1.45; }
</style>
</head>
<body>
<div class="stage">
  <div class="layer" id="layer">
    <span class="dot a"></span><span class="dot b"></span><span class="dot c"></span>
    <h2>Blend</h2>
  </div>
</div>
<div class="modes" id="modes"></div>
<code id="code"></code>
<p id="note"></p>

<script>
  // Every mix-blend-mode value and what it does to the stage behind it
  const modes = {
    'normal': 'No blending. The layer covers what is behind it.',
    'multiply': 'Multiplies the colours. Never lighter. White text disappears, black stays black.',
    'screen': 'The opposite of multiply. Never darker. A black layer would disappear.',
    'overlay': 'Multiply where the backdrop is dark, screen where it is light.',
    'darken': 'Keeps the darker value of each colour channel.',
    'lighten': 'Keeps the lighter value of each colour channel.',
    'color-dodge': 'Brightens the backdrop. Over black it does nothing.',
    'color-burn': 'Darkens the backdrop. Over white it does nothing.',
    'hard-light': 'Multiply or screen, decided by the layer\'s colour.',
    'soft-light': 'A gentler hard-light.',
    'difference': 'Subtracts the darker colour from the lighter one. White text turns into the inverse of the backdrop.',
    'exclusion': 'Like difference, with lower contrast.',
    'hue': 'Hue of the layer, saturation and lightness of the backdrop.',
    'saturation': 'Saturation of the layer, hue and lightness of the backdrop.',
    'color': 'Hue and saturation of the layer, lightness of the backdrop.',
    'luminosity': 'Lightness of the layer, hue and saturation of the backdrop.'
  };
  const layer = document.getElementById('layer');
  const box = document.getElementById('modes');

  function pick(mode) {
    layer.style.mixBlendMode = mode;
    document.getElementById('code').textContent = 'mix-blend-mode: ' + mode + ';';
    document.getElementById('note').textContent = modes[mode];
    box.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', b.textContent === mode));
  }

  Object.keys(modes).forEach(mode => {
    const b = document.createElement('button');
    b.textContent = mode;
    b.addEventListener('click', () => pick(mode));
    box.appendChild(b);
  });
  pick('difference');
</script>
</body>
</html>
All 16 blend modes on one layer. The white band at the top and the black band at the bottom show which modes change light and dark areas.
.layer {
  mix-blend-mode: difference; /* normal, multiply, screen, overlay, ... */
}

Nothing about the layer changes except how it is painted. It keeps its size, its place in the layout and its click area.

mix-blend-mode vs background-blend-mode

The two properties take the same list of modes. What differs is what they blend with.

mix-blend-mode background-blend-mode
Set on Any element, including text An element with background layers
Blends with Content painted behind the element Its own lower background layers and background colour
Reaches other elements Yes, up to the nearest stacking context No
Creates a stacking context Yes, for any value but normal No
Typical use Text over images, overlapping shapes Tinting an image, duotones

If you want to tint one picture, reach for background-blend-mode. If you want an element to react to what scrolls or moves under it, you need mix-blend-mode.

What the blend modes do

The browser blends colour channel by colour channel. The element on top is the source, and what is behind it is the backdrop.

The same backdrop with a solid layer blended over the bottom half, in three modes.
The same backdrop with a solid layer blended over the bottom half, in three modes.
Mode What it does Neutral colour
multiply Multiplies the colours. Never lighter White
screen The reverse of multiply. Never darker Black
overlay Multiply on dark backdrop areas, screen on light ones 50% grey
darken / lighten Keeps the darker or lighter value of each channel White / black
color-dodge / color-burn Brightens or darkens the backdrop strongly Black / white
hard-light / soft-light Darkens or lightens, chosen by the source colour. Soft-light is gentler 50% grey
difference Subtracts the darker colour from the lighter one Black
exclusion Like difference, with lower contrast Black
hue, saturation, color, luminosity Take one part of the source colour and the rest from the backdrop

"Neutral colour" means a source of that colour leaves the backdrop unchanged. That is why white text with multiply vanishes in the first example. color keeps the backdrop's lightness and takes hue and saturation from the source, which makes it good for tinting.

For effects that do not depend on what is behind, such as grayscale or blur, the CSS filter property is the simpler tool.

Contain the blend with isolation: isolate

A blended element does not stop at its parent. It blends with everything painted behind it, up to the nearest stacking context. On a plain page, that can be the page background itself.

Left: the circle blends with the stripes behind its group. Right: isolation: isolate keeps the blend inside the group.
Left: the circle blends with the stripes behind its group. Right: isolation: isolate keeps the blend inside the group.

Add isolation: isolate to the wrapper. The wrapper becomes a stacking context, and its children blend only with each other. Where the wrapper is transparent, a blended child looks as if it had no blend mode.

.card { isolation: isolate; }                /* blending stops here */
.card .shape { mix-blend-mode: multiply; }   /* blends with the card's content only */

The same cut-off happens by accident. opacity below 1, transform, filter and a positioned element with a z-index all create a stacking context too. See CSS z-index.

When a blend seems to do nothing, check the ancestors for these first.

It also works the other way. Any mix-blend-mode other than normal makes the element itself a stacking context. A child with z-index: -1, which used to slide behind the parent's background, now paints above it.

Duotone images with background-blend-mode

A duotone maps the dark parts of an image to one colour and the light parts to another. background-blend-mode does it with no image editing, using three background layers on one element.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>isolation and background-blend-mode</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h3 { margin: 0 0 8px; font-size: 15px; }
  code { font: 600 13px ui-monospace, Consolas, monospace; }

  /* 1. The "page": stripes that sit behind both groups */
  .page {
    display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 14px; border-radius: 12px;
    background: repeating-linear-gradient(45deg, #34c759 0 14px, #00c7fb 14px 28px);
  }
  .group { position: relative; height: 150px; border: 2px dashed #1d2330; border-radius: 10px; }
  .group.isolated { isolation: isolate; }   /* blending stops at this box */
  .label { position: absolute; left: 6px; bottom: 6px; font: 600 12px ui-monospace, Consolas, monospace;
           background: #fff; padding: 3px 6px; border-radius: 6px; }
  .square { position: absolute; left: 10px; top: 12px; width: 70px; height: 70px; background: #ffcc00; }
  .circle {
    position: absolute; left: 44px; top: 36px; width: 84px; height: 84px; border-radius: 50%;
    background: #ff2d92; mix-blend-mode: multiply;
  }

  /* 2. Duotone: a grey "photo" (inline SVG) tinted by two colours in one background */
  .duo {
    --dark: #1b1464; --light: #ff5e7e;
    --photo: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 240' preserveAspectRatio='xMidYMid slice'><defs><linearGradient id='s' x2='0' y2='1'><stop offset='0' stop-color='%23888'/><stop offset='1' stop-color='%23eee'/></linearGradient></defs><rect width='400' height='240' fill='url(%23s)'/><circle cx='290' cy='84' r='36' fill='%23fff'/><path d='M0 170 L90 86 L160 150 L230 64 L330 160 L400 116 V240 H0Z' fill='%23666'/><path d='M0 204 L120 150 L220 196 L310 158 L400 196 V240 H0Z' fill='%23222'/></svg>");
    height: 200px; max-width: 440px; border-radius: 12px; margin-top: 10px;
    background-image:
      linear-gradient(var(--dark), var(--dark)),  /* shadows colour */
      var(--photo);                               /* the grey image */
    background-size: cover;
    background-position: center;
    background-color: var(--light);
    background-blend-mode: lighten, multiply;  /* one value per image layer, top first */
  }
  .duo.original { background-image: var(--photo); background-blend-mode: normal; }
  .controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-top: 10px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 6px; }
  input[type=color] { width: 40px; height: 30px; padding: 0; border: 1px solid #c9cdd4; border-radius: 6px; background: #fff; }
</style>
</head>
<body>
<h3>1. mix-blend-mode, with and without <code>isolation</code></h3>
<div class="page">
  <div class="group">
    <div class="square"></div><div class="circle"></div>
    <span class="label">no isolation</span>
  </div>
  <div class="group isolated">
    <div class="square"></div><div class="circle"></div>
    <span class="label">isolation: isolate</span>
  </div>
</div>

<h3 style="margin-top:16px">2. Duotone with <code>background-blend-mode</code></h3>
<div class="duo" id="duo" role="img" aria-label="Mountains and sun in two colours"></div>
<div class="controls">
  <label>Shadows <input type="color" id="dark" value="#1b1464"></label>
  <label>Highlights <input type="color" id="light" value="#ff5e7e"></label>
  <label><input type="checkbox" id="orig"> Show the grey original</label>
</div>

<script>
  const duo = document.getElementById('duo');
  // The two colours are custom properties, so the inputs only need to set them
  document.getElementById('dark').addEventListener('input', e => duo.style.setProperty('--dark', e.target.value));
  document.getElementById('light').addEventListener('input', e => duo.style.setProperty('--light', e.target.value));
  document.getElementById('orig').addEventListener('change', e => duo.classList.toggle('original', e.target.checked));
</script>
</body>
</html>
Top: the same group with and without isolation. Bottom: a grey image turned into a duotone. Pick both colours.
.duo {
  background-image:
    linear-gradient(#1b1464, #1b1464),   /* shadow colour */
    url(photo.jpg);                      /* a grey or greyscale image */
  background-color: #ff5e7e;             /* highlight colour */
  background-size: cover;
  background-blend-mode: lighten, multiply;
}

The modes are listed top layer first, one per image. Bottom up, the image multiplys with the pink background colour: white becomes pink, black stays black. Then the dark gradient lightens the result, lifting the blacks to dark blue.

This works best on a grey image. A colour photo still tints, but its own colours mix in. How background layers stack and size is covered in CSS background image.

Text that inverts over any background

White text with mix-blend-mode: difference shows the inverse of whatever is behind each letter. It is black over white, white over black, blue over yellow. One headline stays visible over stripes, photos and moving backgrounds.

Difference text is sharp over dark, light and strong colours, and fades out over mid-tones.
Difference text is sharp over dark, light and strong colours, and fades out over mid-tones.

The catch is mid-grey. The inverse of a colour near the middle is another colour near the middle, so the text almost disappears. This is a real readability problem, not just a looks problem:

  • Build the backdrop from dark, light and saturated colours.
  • Check the text over the darkest, lightest and greyest spots.
  • Keep small text out of blend modes. Put it on a solid panel.
  • Stop a moving backdrop under prefers-reduced-motion: reduce.

A finished example

The hero below has a moving backdrop of black and white stripes with yellow and cyan bands. The headline is white with difference, and the hero has isolation: isolate so the blend stays inside it. The cards are duotones of one grey image.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blend-mode hero and duotone cards</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }

  /* Hero: its own stacking context, so the headline only blends with the hero */
  .hero {
    position: relative; height: 250px; border-radius: 14px; overflow: hidden;
    isolation: isolate; background: #000;
  }
  /* Moving backdrop built only from black, white and strong colours: no mid-grey */
  .bg {
    position: absolute; inset: 0;
    background:
      linear-gradient(100deg, transparent 0 30%, #ffd400 30% 48%, transparent 48% 70%, #00c7fb 70% 84%, transparent 84%),
      repeating-linear-gradient(-45deg, #000 0 40px, #fff 40px 80px);
    background-size: 200% 100%, 226.274px 226.274px;  /* 80px stripes x sqrt(2) x 2: tiles without seams */
    animation: slide 12s linear infinite;
  }
  @keyframes slide { to { background-position: -200% 0, 226.274px 0; } }
  @media (prefers-reduced-motion: reduce) { .bg { animation: none; } }

  .hero h1 {
    position: relative; margin: 0; padding: 40px 20px 0;
    font: 900 clamp(40px, 12vw, 76px)/1 system-ui, sans-serif;
    color: #fff;
    mix-blend-mode: difference;   /* white text = inverse of whatever is behind it */
  }
  /* Small text does not blend: it sits on a solid chip */
  .hero p {
    position: absolute; left: 20px; bottom: 18px; margin: 0; max-width: 80%;
    background: #fff; color: #1d2330; padding: 6px 10px; border-radius: 8px; font-size: 14px;
  }

  /* Duotone cards: same grey image, different colour pair per card */
  .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; margin-top: 12px; }
  .card {
    --photo: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 220' preserveAspectRatio='xMidYMid slice'><defs><linearGradient id='s' x2='0' y2='1'><stop offset='0' stop-color='%23777'/><stop offset='1' stop-color='%23f2f2f2'/></linearGradient></defs><rect width='300' height='220' fill='url(%23s)'/><circle cx='210' cy='70' r='30' fill='%23fff'/><path d='M0 150 L70 80 L130 130 L190 60 L300 140 V220 H0Z' fill='%23555'/><path d='M0 185 L100 140 L180 180 L300 150 V220 H0Z' fill='%23111'/></svg>");
    border: 0; padding: 0; border-radius: 12px; overflow: hidden; cursor: pointer;
    text-align: left; font: inherit; background: #fff; box-shadow: 0 4px 14px rgba(0, 0, 0, .1);
  }
  .img {
    height: 120px;
    background-image: linear-gradient(var(--dark), var(--dark)), var(--photo);
    background-size: cover; background-position: center;
    background-color: var(--light);
    background-blend-mode: lighten, multiply;
  }
  .card[aria-pressed="true"] .img { background-image: var(--photo); background-blend-mode: normal; }
  .card span { display: block; padding: 8px 10px; font-size: 13px; }
  .hint { font-size: 13px; color: #5b6270; margin: 8px 0 0; }
</style>
</head>
<body>
<header class="hero">
  <div class="bg"></div>
  <h1>Readable on any stripe</h1>
  <p>The headline is white with <b>mix-blend-mode: difference</b>.</p>
</header>

<div class="cards">
  <button class="card" aria-pressed="false" style="--dark:#1b1464; --light:#ff5e7e"><div class="img"></div><span>Sunset</span></button>
  <button class="card" aria-pressed="false" style="--dark:#003b36; --light:#9cf6c7"><div class="img"></div><span>Mint</span></button>
  <button class="card" aria-pressed="false" style="--dark:#3a0ca3; --light:#ffd166"><div class="img"></div><span>Gold</span></button>
</div>
<p class="hint">Tap a card to see the grey original.</p>

<script>
  document.querySelectorAll('.card').forEach(card => {
    card.addEventListener('click', () => {
      const on = card.getAttribute('aria-pressed') === 'true';
      card.setAttribute('aria-pressed', !on);
    });
  });
</script>
</body>
</html>
Headline with difference over a moving backdrop, and three duotone cards. Tap a card to see the original.
  • Backdrop: two animated gradient layers with no mid-grey, so the headline stays readable.
  • Small text: sits on a white chip with no blend mode.
  • Cards: each card sets --dark and --light inline for one shared duotone rule.

A frosted panel is a different effect. It blurs what is behind rather than mixing colours, and glassmorphism CSS covers it.

When it does not work

What you see Cause Fix
Nothing changes Nothing is painted behind the element in its stacking context Give the parent, or body, a background
Blending works on the page but not inside a card An ancestor has opacity, transform, filter or z-index, which isolates it Move the blended element, or blend inside that ancestor
Other elements get tinted too The blend reaches the page background isolation: isolate on the wrapper
A solid rectangle blends, not just the text The text element has its own background Remove the background from the blended element
Blended text is hard to read Mid-tone backdrop Change the backdrop colours, or drop the blend for that text
A child with negative z-index jumped in front Blend modes create a stacking context Restructure, or blend a sibling instead

The first row has a subtle case. In Chromium, a blended element on a page with no background colour at all showed no blending in our test. Setting background: #fff on body fixed it.

Blend modes react to what is behind them, so a screenshot shows one moment and one screen size. A shared page lets people scroll, pick colours and watch the headline change over the stripes.

Paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can try every mode themselves. If you change the code later, the same link shows the new version.

Questions people ask

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

mix-blend-mode blends a whole element, text and all, with the content painted behind it. background-blend-mode only blends the background layers of one element with each other and with its background colour. It never reaches content outside the element.

Why is mix-blend-mode not working?

Usually there is nothing behind the element to blend with. The element blends only with content inside the same stacking context, so an ancestor with opacity, transform, filter or isolation: isolate cuts it off from the page behind. A page with no background colour can also leave nothing to blend with.

How do I make text that changes colour over any background?

Make the text white and give it mix-blend-mode: difference. Over black it stays white, over white it turns black, and over a colour it becomes the inverse colour. Over mid-grey it nearly disappears, so keep the backdrop away from mid-tones.

Does mix-blend-mode create a stacking context?

Yes. Any value other than normal makes the element a new stacking context, the same way opacity below 1 does. Its z-index children are then layered inside it rather than with the rest of the page.

Can mix-blend-mode be animated?

Not smoothly. It switches from one mode to the next with no in-between steps. To fade an effect in, animate the opacity or colour of the blended layer instead.

Keep reading