It started with seeing a recent Pen of Mandy Michael’s text effects demos. I’m a very visual creature, so the first thing I noticed was the effect, not the title (which clearly states how the effect was achieved). Instantly, my mind went “blend modes!”, which turned out to be wrong.
The demo actually uses clip-path
. First of all, the text is duplicated. We have black text below as the actual text content of the element and the white text above as the value of the content
property (taken from a data attribute which gets updated via JS). These two are stacked one on top of each other (they completely overlap). Then the pseudo-element with the white text above gets clipped to the shape of the black dress.
However, this means we need to change the clipping path if we change the image and, at this point, it’s anything but easy to figure out polygonal clipping paths with a lot of points via dev tools (which is why having something like Benett Feely’s Clippy with two-way editing directly in dev tools would be immensely useful). So I decided to give my initial idea – blend modes – a try.
Let’s say we have a heading in a contentEditable
1 container with a black and white image (well, grayscale) background
. The HTML structure is as follows:
<header> <h2 contentEditable role='textbox' aria-multiline='true'>And stay alive</h2>
</header>
We set the background-image
on the header
container, we give the h2
white text and set its mix-blend-mode
to difference
or exclusion
. The relevant CSS is below:
header { background: url(black-and-white-image.jpg) } h2 { color: white; mix-blend-mode: difference;
}
I can’t really understand blend modes or normally see a difference between difference
and exclusion
, but according to MDN, they’re pretty much the same, with exclusion
having less contrast. What I can understand in this situation is that having white text over the image gives us a result that’s the image inverted where the text overlaps it. For simple black and white images, black in the original image becomes white where we have white text above it and white in the original image becomes black where we have white text above it.
The result can be seen in the following Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Mission accomplished! Both the text and the image can be changed and the effect is preserved without the need for any JavaScript or for any changes to the CSS.
But I instantly found that there was another itch to scratch: what happens if the image isn’t just black and white? Well, let’s try that! Turns out the result actually looks pretty good:
See the Pen by thebabydino (@thebabydino) on CodePen.
However, the text isn’t grayscale anymore and setting filter: grayscale(1)
doesn’t change anything. Does any filter
value work? Well, tried drop-shadow()
next to try to find an answer to this question. And the answer is that, yes, drop-shadow()
works, but the way it works – the shadow being blended with the header
background2 – provides a clue about why grayscale()
didn’t change a thing: filters are applied before blending, our text is white and the output of grayscale()
in this case is identical to its input (white in, white out). And, since nothing changes before the blending, its result is the same.
See the Pen by thebabydino (@thebabydino) on CodePen.
This probably shouldn’t have been a surprise. I’ve ran into issues caused by the order in which properties are applied before.
For example, filter
is also applied before clip-path
, so if we clip an element to a non-rectangular shape and we want to have a drop shadow on it, tough luck, setting the filter
on the same element doesn’t give the expected result. This is because the rectangular element gets the filter
applied, so we have a drop shadow around its rectangular box and only after that gets clipped to the shape specified via clip-path
. This is always the order these two get applied in, regardless of the order you set them in the CSS.

filter
and clip-path
when they’re set on the same element.The way to solve the filter
+ clip-path
problem is to set the filter
on a parent element with nothing visible except the clipped child. The “nothing visible” part is important because, if the parent has some visible text or borders, they’ll get the drop shadow as well, while if it has a background, then the whole background area gets the shadow.
See the Pen by thebabydino (@thebabydino) on CodePen.
However, this solution doesn’t work for the filter
+ mix-blend-mode
problem as well. Wrapping our h2
into a div
and setting filter
on that div
breaks the blending effect.
See the Pen by thebabydino (@thebabydino) on CodePen.
What’s worse is that I can’t seem to think of anything I could do to get around this problem. There might be a way of doing it by duplicating the text and using a luminosity
blend mode, but, as mentioned before, I don’t really understand blend modes and I haven’t been able to get that right.
But maybe we could try a different approach altogether, one that doesn’t use blending. All the playing with filters gave me the idea that we could apply the image to the text, then apply an invert()
filter, to which we could chain grayscale()
, contrast()
and more.
With this method using background-clip: text
, we also have the advantage of a cross-browser solution, since Firefox and Edge have now implemented this too.
The way we go about this is the following: we make sure the header
and the h2
have identical backgrounds and that these backgrounds perfectly overlap. Then we set color: transparent
on the h2
and clip its background to text
. The final step is to set filter: invert(1)
on the h2
. The relevant CSS3 is as follows:
h2 { background: inherit; background-clip: text; color: transparent; filter: invert(1);
}
The result can be seen in the following Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
It looks like before, except it’s using a different method and now we can chain more functions to the filter
property. For example, we can make the text grayscale and up its contrast:
filter: invert(1) grayscale(1) contrast(9)
See the Pen by thebabydino (@thebabydino) on CodePen.
Tweaking the filter
value is also how we add a text shadow. While in the first case (using mix-blend-mode
) we can simply set the text-shadow
property (and the shadow also gets blended with the background), doing so in the second case breaks things:
See the Pen by thebabydino (@thebabydino) on CodePen.
Fortunately, we can chain drop-shadow()
to our filter.
See the Pen by thebabydino (@thebabydino) on CodePen.
The Pen below shows the two methods described in this article. On the left, we have the mix-blend-mode
method and on the right, we have the background-clip
and filter
method (with the extra grayscale and contrast components). The text is editable and the thumbnails allow for changing the background-image
:
See the Pen by thebabydino (@thebabydino) on CodePen.
So, if we pit these methods against each other, which is better?
When don’t go into aesthetics because it’s a subjective matter and it probably differs from case to case and even from mood to mood.
As far as basic support is concerned, the last method gets an advantage since it’s supported in Edge, while mix-blend-mode
isn’t (but if you want it in Edge, please vote for it because your feedback does matter). Mandy’s original example uses clip-path
, another very useful feature, which sadly doesn’t work in Edge either (you can vote for it here).
When it comes to selecting text, the mix-blend-mode
method works perfectly and looks the best across the browsers it’s supported.

mix-blend-mode
methodUsing the clip-path
method, the selection looks clean and the text remains readable, but it seemed to stumble in Firefox while I was over the pseudo-element text. Fortunately, this problem turned out to have an easy fix: setting pointer-events: none
on the pseudo-element.

clip-path
methodAs for the last method, the selection can look ugly and can make the text harder to read. Not to mention that the drop shadow ends up being applied to the whole selection rectangle in this case, not just to the actual text.

background-clip: text
+ filter
methodChanging the text also gets awkward in Firefox with the last method, the screen getting “dirty” as I type new text.

background-clip: text
+ filter
methodThe clip-path
method also had a problem with changing text in Firefox at first: trying to start editing from the middle of the pseudo-element text didn’t work. Fortunately, setting pointer-events: none
on the pseudo-element fixes this as well.

clip-path
methodAs far as flexibility in terms of how we can alter the text goes, the last method fares best because chaining filter functions can take us a long way.
At the end of the day, which is better ends up depending on the particular use case. What browsers should be supported? Does the image change? Does the text change? How should the text be visually altered?
1 contentEditable
isn’t accessible in all browsers by default, so we need to provide semantics to fix that.
2 When experimenting with blend modes, be aware of the legibility issues they can produce, particularly regarding contrast. WCAG 2.0 states that, unless the text forms part of purely decorative imagery, it should pass a minimum contrast threshold. Tools like Color ContrastAnalyzer can help you meet that requirement.
3 Prefixes are omitted for brevity, but background-clip
still needs the -webkit-
prefix for WebKit browsers (Firefox and Edge have implemented it unprefixed, though they both support it with the -webkit-
prefix as well, probably because it has already been used to death only with that prefix) and this prefix needs to be added manually/ via a preprocessor mixin, which is something that I normally don’t encourage, but, in this particular case, auto-prefixing via Autoprefixer or Prefixfree doesn’t happen (Prefixfree works via feature detection, which doesn’t catch properties such as background-clip
, filter
or clip-path
and this is the Autoprefixer issue). As for filter
, it’s now supported unprefixed in all current desktop browsers and Autoprefixer can prefix it anyway.
Methods for Contrasting Text Against Backgrounds is a post from CSS-Tricks