Blend Modes: A Mathematical Approach to difference and exclusion

Blend modes in CSS have always had a reputation for being unpredictable. The usual path to learning them involves trial and error, Photoshop comparisons, or abstract artistic language — none of which give a developer a solid mental model. But the CSS spec includes something far more useful: the mathematical formulas underlying each mode.

This article kicks off a series that approaches blending from that technical angle. You'll learn the general principles of how blending works in CSS, then apply them to two related modes — difference and exclusion — and see how those formulas create practical effects.

Illustration showing two layers. The top layer is the source, while the bottom one is the destination.
Blending terminology

How Blending Actually Works

At its core, blending combines two stacked layers into a single visual result. When those two layers are sibling elements (including pseudo-elements or text), the property is mix-blend-mode. When they are background layers on the same element — including the background-color — the property is background-blend-mode.

The terminology can be confusing: the top layer is the source, and the bottom is the destination. Both are inputs; the output is the blended result. These layers are always processed pixel by pixel. If you stack more than two layers, blending happens sequentially from the bottom upward. The lowest pair blends first, and that result becomes the destination for the next layer above it.

Illustration showing the process described above.
Blending multiple layers

The difference and exclusion modes share several useful properties. First, they are commutative: the result doesn't change depending on which layer is on top. Second, they are separable — each color channel (red, green, blue) is computed independently using only its own values from the source and destination layers.

Ch = fB(Chs, Chd)

In the formulas, color channel values are expected as decimals between 0 and 1. So a pixel of rgb(220, 20, 60) — known as crimson — is treated as approximately 0.863, 0.078, 0.235. Pure black is 0 across all channels, and white is 1. Transparency (alpha: 0) causes the result to match the other layer exactly.

The difference Blend Mode

The function behind difference is as simple as the name suggests — it takes the absolute value of the difference between the source and destination channels.

Ch = fB(Chs, Chd) = |Chs - Chd|

Because the result is always positive, overlapping pixels of identical color become black (all zeros). And because subtracting zero changes nothing, blending with pure black leaves the other layer untouched. Conversely, since subtracting from 1 produces the complement of a value, blending with pure white inverts the other layer, much like filter: invert(1).

Ch = fB(0, Chd) = |0 - Chd| = |-Chd| = Chd
Ch = fB(1, Chd) = |1 - Chd| = 1 - Chd

The exclusion Blend Mode

The formula for exclusion is different but hits the same key landmarks. It is:

Ch = fB(Chs, Chd) = Chs + Chd - 2·Chs·Chd

With a black layer, both the product and the sum reduce to the other layer's channel value, so the other layer shows through unchanged. With a white layer, the result simplifies to 1 - Ch, which is the complement of the other layer's value — a full inversion just like difference.

Ch = fB(0, Chd) = 0 + Chd - 2·0·Chd = Chd - 0 = Chd
Ch = fB(1, Chd) = 1 + Chd - 2·1·Chd = 1 + Chd - 2·Chd = 1 - Chd

The behavioral distinction appears when working with pixels that are not strictly black or white. Despite different formulas, difference and exclusion produce identical output whenever at least one of the layers' pixels is purely black or white.

State-Change Text Effects

One elegant application combines the difference formula with a link and its absolutely positioned pseudo-element. With both the link text and the pseudo-element set to the same color and background, blending them yields black text within the highlight. The effect reacts to :hover and :focus states, and becomes an reveal animation by scaling the pseudo-element from a thin line.

p { isolation: isolate; }

a {
  /* same as before */
  
  &::after {
    /* same as before */
    mix-blend-mode: difference;
  }
}
a {
  /* same as before */
  
  &::after {
    /* same as before */
    transition: transform .25s;
  }
}

This approach has a few practical wrinkles. For example, the paragraph must set isolation: isolate to prevent blending with the parent body in Firefox — Chrome currently behaves incorrectly per the spec, so it doesn't need it today. Text descenders can stick out of the highlight, so add horizontal padding to the link. For light themes, the trick is to invert the entire visual output: set the colors for a dark theme (the inverse), then apply filter: invert(1) on a wrapper. Be aware the filter will invert all descendants, so you'll need to reverse the inversion on any img elements inside.

section {
  /* same as before */
  
  &, & img { filter: invert(1); }
}

Wrapping links pose a second challenge. Instead of blending text content with a pseudo-element, you can blend two background layers directly on the link itself. The first layer, clipped to text, reveals the "filled" part; the second, clipped to border-box, grows vertically on hover and focus. Moving to this approach also brings its own bugs, though. Firefox doesn't blend background layers at all due to a longstanding bug, and Chrome will unexpectedly blend inline elements with the parent's background unless you use an opacity value just below 1 as a hack. The text edges themselves may appear cut off, but setting box-decoration-break: clone — with its -webkit- prefix where needed — fixes the corners.

Animated gif. Shows the result when we blend two background layers on the actual link: one clipped to text and the other one clipped to border-box.
The background layer blending solution (demo).

Inverting Exactly What You Want

Duplicating elements and using clip-path can invert part of a page, but neither approach works if you only want to invert a background and leave the content alone. Blending solves this problem cleanly. Stack a couple of background layers: one with your image, and one — a gradient — that's white where you want inversion and transparent elsewhere. Blending them with exclusion gives you the inverted region. The untouched text remains.

div {
  background: 
    linear-gradient(45deg, white 50%, transparent 0), 
    url(cat.jpg) 50%/ cover;
}

An immediately useful instance of this is a single-element image comparison slider built with a range input. The trick lies in positioning a split gradient so the separation point follows the thumb's current value, expressed via a custom property updated with a couple of lines of JavaScript.

@mixin track() {
  /* same as before */
  background:
    url(flowers.jpg) 50%/ cover, 
    linear-gradient(90deg, transparent var(--p), white 0);
  background-blend-mode: exclusion;
}

[type='range'] {
  /* same as before */
  --p: calc(var(--k) * 1%);
}
addEventListener('input', e => {
  let _t = e.target;
  _t.style.setProperty('--k', +_t.value)
})

To make the line hit the exact edges, the thumb's travel must be accounted for by extending the input's width beyond the viewport by one full thumb width and adjusting the gradient stop's range. A vertical slider works on the same premise, except you must place the gradient on a rotated parent and keep the actual input element — and its own track background — transparent.

body { overflow: hidden; }

[type='range'] {
  /* same as before */
  margin-left: -$thumb-r;
  width: calc(100vw + #{$thumb-w});
}

Gradual and Partial Inversion Effects

Blend modes also enable partial inversion — a level of inversing between 0 and 100% — which is more powerful than only flipping or leaving alone. Working through the math of filter: invert(p), the effect is exactly reproduced by blending with the rgb color rgb(p, p, p) using the exclusion mode. This property allows smooth longitudinal gradient inversions over an image or the background via a gradient from black to white. Using difference here would not be equivalent; it generates a stark, duotone-like result that passes through RGB zero points rather than the expected mid-grey.

background: 
  url(butterfly_blues.jpg) 50%/ cover, 
  linear-gradient(90deg, 
    #000 /* equivalent to rgb(0%, 0%, 0%) and hsl(0, 0%, 0%) */, 
    #fff /* equivalent to rgb(100%, 100%, 100%) and hsl(0, 0%, 100%) */);
background-blend-mode: exclusion;

The same idea extends to using any black-and-white image layer for inversion. Black areas preserve the background color, white areas invert it fully, and all shades in between produce a proportional effect with exclusion.

The XOR Effect

When both layers are limited to black and white pixels, both formulas behave like a boolean XOR: overlap of different colors yields white, overlap of the same colors yields black. This creates hollow grid, ripple, and stripe patterns.

Two overlapping radial patterns produce square-like cells in their intersection. Outside of Chromium, using mix-blend-mode in this setup can engage blending against a page's background in a way you don't want. You can resolve this by isolating the parent, removing the blend declaration from the lowest pseudo, or sidestepping the problem entirely by blending two background layers with background-blend-mode.

$d: 15em;
$u0: 10%;
$e0: 1px;
$u1: 5%;
$e1: .2%;

div {
  width: $d;
  height: $d;
  background: 
    repeating-radial-gradient(
      #000 0 calc(#{$u0} - #{$e0}), 
      #fff $u0 calc(#{2*$u0} - #{$e0}), 
      #000 2*$u0), 
    repeating-conic-gradient(
      #000 0% $u1 - $e1, 
      #fff $u1 2*$u1 - $e1, 
      #000 2*$u1);;
  background-blend-mode: exclusion;
}

Given the ability to swap layers and shapes, the effect becomes quite flexible. The classic animated "split screen" is a moving white disc over half-black, half-white static scene. Animate the backdrop and item — each as a separate pseudo-element — with individual transforms to create butterfly or moon crossings without changing the blending math.

Animated gif. Shows a white triangle oscillating left and right (while also rotating and being squished) and being XORed with the background that's half white (thus making the triangle black) and half black (leaving the triangle white).
Oscillating and rotating plastic shape (demo)

By introducing `contrast` or `invert` filters on a wrapper (as with a light grey body versus dark grey shapes), the monochrome interaction can be given a softer, grayscale look. And it works with complex relationships like moving shapes that collide and create highlights. An offset frame that appears to carve out a hollow path through another shape is the same idea. Hover and click states reproduce correctly on those intersections.

Bringing Color into the Mix

Pure black and white is not a limitation once you layer on other CSS effects. A sepia() filter after reducing contrast adds color to areas that previously fell mid-grey, and hue-rotate() and saturate() give precise control. SVG filters offer another avenue for custom color manipulation.

Perhaps most direct is placing an additional pure color layer beneath or above your black-and-white composited content. Blending a wrapper that contains the XOR result with a solid purple background turns the black into purple and the white into its complementary green. This puts the creative freedom squarely into your layer stacking without touching the composition's core math.

For multi-color works, difference has another trick. Overlap identical colors produce black intersections. The offset frame effect can be set up to literally "cut" a transparent hole with a coral frame against a coral background, which works just as effectively over a gradient or image background with background-clip applied.

The Pattern is Just Beginning

The power of these two modes lies exactly where technical specifications shine: unexpected simplicity yields predictable, broad results. Every use case here is achievable without canvas or SVG replacements. Future installments will extend this framework to other blend modes, combining them for layered effects your current toolkit would never produce.