Can I do knock-out/punch-through transparency with CSS fonts?
Asked Answered
W

7

12

I would like to know if there is any way I can apply 100% transparency to text so that we can see the background picture of the page within the characters in the text.

i.e. imagine I’ve got a <div> with a white background, and a background image on <body>. I’d like to set the text inside the <div> so that the background image on <body> can be seen through the text, despite the white background on the <div>.

I could probably use an inverted font but I would prefer a better way to do it if there is one.

Walters answered 18/2, 2010 at 14:11 Comment(0)
B
10

Does it have to be dynamic? The only way to do that is with an image with transparency (GIF or, better, PNG).

I'm not sure if this is what you want, but will explain it anyway.

Situation: you have a non plain background that you want to bee seen through your text.

Solution: no CSS is coming to the rescue for this. You'll have to use your trusty image editor to create a layer with text, and another layer that will be the negative of your text

This could allow you to have some interesting effects, but if you want it to be dynamic, you'll have to generate the images on the fly serverside.

This kind of trickery is currently impossible with pure CSS (might be possible with Javascript).


Edit

Seeing Paul's find on webkit got me thinking on how to fake that behavior in Firefox, Opera and IE. So far I've had good luck using the canvas element on Firefox, and I'm trying to find some behavior in filter:progid:DXImageTransform.Microsoft.

So far with canvas, this is what I did

<html>
<body>
<canvas id="c" width="150" height="150">
</canvas>
<script>
ctx = document.getElementById("c").getContext("2d");
// draw rectangle filling the canvas element
ctx.fillStyle = "red";
ctx.fillRect(0,0,150,150);

// set composite property
ctx.globalCompositeOperation = 'destination-out'; 
// the text to be added now will "crop out" the red rectangle
ctx.strokeText("Cropping the", 10, 20);  
ctx.strokeText("red rectangle", 10, 40);  

</script>
</body>
</html>

by using a detination-out compositing and drawing text on the canvas.

Barthol answered 18/2, 2010 at 14:49 Comment(3)
Oh! Now I get what mnml was asking. Sorry, seems obvious now.Kurgan
For the record, I think this is called "punch-through" or "knockout" transparency - see e.g. help.adobe.com/en_US/photoshop/cs/using/… (and https://mcmap.net/q/909587/-how-do-i-make-punch-through-transparent-text-in-html-css/20578).Kurgan
Eugh, all your images no longer work. I don't suppose you can re-upload any replacements using the Stack Exchange image uploader? In the meantime, I've edited the images out, but tried to keep the value of the post... obviously, feel free to edit anything back in if I've somehow invalidated your points (and sorry if I did!).Ballard
A
22

The state of things in 2023 - in short, we have the following options:

  • Pure CSS with mix-blend-mode: limited to fully white/ black backgrounds on the p when using the screen/ multiply blend modes, or at best all 3 channels of the background on the p having higher/ lower values than the corresponding channels of any pixels of the body background image when using lighten/ darken
  • Pure CSS with background-clip: text: while this has evolved from a non-standard feature into a standard one and support has massively improved, it's still limited by alignment to the body background + involves nesting due to buggy behaviour in Firefox
  • Using an SVG filter to make the text pixels transparent: has excellent support all the way back to IE9 and not many limitations

In detail:

Pure CSS: mix-blend-mode

This is very straightforward, the relevant code is just:

p {
    background: #fff;
    color: #000;
    mix-blend-mode: screen
}

Anything else is just unrelated layout/ prettifying fluff, not affecting getting the effect we want.

Note the snippet below only works properly if you have a fully white background (which may or may not have semi-transparency).

html, body { display: grid }

html { height: 100% }

body {
  background: url(https://images.unsplash.com/photo-1506383631675-0b110111327b?w=1400) 50%/ cover
}

p {
  place-self: center;
  padding: 0 0.125em;
  width: min-content;
  background: #fff;
  font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
  text-align: center;
  mix-blend-mode: screen
}
<p contenteditable>Hello, world!</p>

What's happening here?

Blending is an operation applied when we have two layers one on top of the other, on a per pixel and, in some cases (those we care about here), per channel basis.

Illustration showing two corresponding pixels of the two layers being blended, which results in the corresponding pixel of the resulting layer.

What any blend mode we might consider for such an effect does is th following: take every pixel of the element it's applied on (the p in this case) and the corresponding pixel of what's behind (the body background image in this case) and perform a computation involving the decimal representations of the percentage RGB channel values of the two corresponding pixels.

How the screen blend mode works

In the case of the screen blend mode, if C₁ is a generic channel (R,G, or B, doesn't matter, the formula is the same) of a top layer pixel and C₀ is a generic channel value of the corresponding pixel in the bottom layer, the formula is as follows:

1 - (1 - C₁)·(1 - C₀)

Let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a black one, corresponding to the actual text (that is rgb(0%, 0%, 0%), all three channel values at 0%). Let's say the corresponding pixel in the body background image is rgb(41%, 55%, 91%) - a sort of greyish blue.

Then the resulting RGB channels of that pixel after the screen blend mode operation are:

1 - (1 - 0)·(1 - .41) = 1 - 1·.59 = 1 - .59 = .41
1 - (1 - 0)·(1 - .55) = 1 - 1·.45 = 1 - .45 = .55
1 - (1 - 0)·(1 - .91) = 1 - 1·.09 = 1 - .09 = .91

That is, the exact same channel values as for the pixel in the body background image!

In general, when a pixel in one of the two layers is black and we use the screen blend mode, then the result of blending is the RGB value of the pixel in the other layer (not the one with the black pixel).

This is because when we have a black pixel, all its three channels are at 0%, 0 in decimal representation, so either the 1 - C₁ or the 1 - C₀ part (depending in which of the two layers the pixels is black) is 1 - 0 = 1, thus making the entire product part equal to the other factor, which is the complement of the channel value of the pixel in the other layer. And then complementing the complement (1 - (1 - a) = a), we get the initial value of the channel in the other layer.

Now let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a white one, corresponding to the background (that is rgb(100%, 100%, 100%), all three channel values maxed out at 100%). Let's say the corresponding pixel in the background image the same greyish blue rgb(41%, 55%, 91%).

Then the resulting RGB channels of that pixel after the screen blend mode operation are:

1 - (1 - 1)·(1 - .41) = 1 - 0·.59 = 1 - 0 = 1
1 - (1 - 1)·(1 - .55) = 1 - 0·.45 = 1 - 0 = 1
1 - (1 - 1)·(1 - .91) = 1 - 0·.09 = 1 - 0 = 1

That is, rgb(100%, 100%, 100%) or white!

In general, when a pixel in one of the two layers is white and we use the screen blend mode, then the result of blending is also white.

This is because when we have a white pixel, all its three channels are maxed out at 100%, 1 in decimal representation, so either the 1 - C₁ or the 1 - C₀ part (depending in which of the two layers the pixel is white) is 1 - 1 = 0, thus making the entire product part 0 and the end result 1 (or 100%). And all three channel values maxed out at 100%, that means white.

Where the screen approach breaks

This also means that for any background of our p that's not fully white, this approach fails.

Let's say our p has a peachpuff background, something that's still very light to contrast with a dark background, that is rgb(100%, 86%, 73%). Replacing in our formula (and considering the same RGB value for the corresponding background pixel), we have the following for the three channels:

1 - (1 - 1)·(1 - .41) = 1 - 0·.59 = 1 - 0 = 1
1 - (1 - .86)·(1 - .55) = 1 - .14·.45 = 1 - .063 = .937
1 - (1 - .73)·(1 - .91) = 1 - .17·.09 = 1 - .0153 = .9847

So the result is rgb(100%, 93.7%, 98.47%), which is different from the peachpuff background we started with, rgb(100%, 86%, 73%). They're both reddish and very light, but they're different and this difference depends on the RGB channels of the body background underneath.

The screenshot below illustrates this problem we run into if we want any background that's not fully white for our p:

Screenshot illustrating how the p background, now not fully white anymore, is blended with the body background underneath to create a peachpuff tinted version of it.

And this peachpuff is a very light, a very close to white option for the background of our p. The darker the background, the more obvious the problem. Here's crimson in action:

Screenshot illustrating how the background blending is worse/ more obvious with a darker p background.

Yikes!

lighten - maybe another acceptable option?

Now if the body background image is very dark (just dark, but around the same channel(s) as the background we want for our p might work too), we might get away with changing the blend mode to lighten.

For example, something like below, where the body background image is just dark greens, while the we've set background: lightgreen for our p:

html, body { display: grid }

html { height: 100% }

body {
  background: url(https://images.unsplash.com/photo-1533563906091-fdfdffc3e3c4?w=1400) 50%/ cover
}

p {
  place-self: center;
  padding: 0 0.125em;
  width: min-content;
  background: lightgreen;
  font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
  text-align: center;
  mix-blend-mode: lighten
}
<p contenteditable>Hello, world!</p>

How the lighten blend mode works

In the case of the lighten blend mode, if C₁ is a generic channel (R,G, or B, again, it doesn't matter, the formula is the same) of a top layer pixel and C₀ is a generic channel value of the corresponding pixel in the bottom layer, the formula is as follows:

max(C₁, C₀)

Really, that's it!

Let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a black one, corresponding to the actual text (that is rgb(0%, 0%, 0%), all three channel values at 0%). Let's say the corresponding pixel in the body background image is rgb(4%, 13%, 6%) - a very dark green.

Then the resulting RGB channels of that pixel after the lighten blend mode operation are:

max(0, .04) = .04
max(0, .13) = .13
max(0, .06) = .06

Again that's the exact same channel values as for the pixel in the body background image!

This is because when we have a black pixel, all its three channels are at 0%, 0 in decimal representation. And any other channel value, which is always in the [0, 1] range is at least as big as 0 - any number from 0 to 1 is greater or equal to 0. So the maximum between 0 and a value in the [0, 1] interval is always equal to the value in the [0, 1] interval.

Now let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a lightgreen one, corresponding to the background of the p (that is rgb(57%, 93%, 57%)). Let's say the corresponding pixel in the background image is the same very dark green - rgb(4%, 13%, 6%).

Then the resulting RGB channels of that pixel after the lighten blend mode operation are:

max(.57, .04) = .57
max(.93, .13) = .93
max(.57, .06) = .57

That's the same as the lightgreen we chose as the background of the p!

Now the background we've chosen here for the body is very dark, which means the channel values of its pixels are very close to zero, so we have a pretty good chance of making this work with a lot of background options for our p.

For example, even if we were to chose something like hotpink for the background of our p, its channel values (rgb(100%, 41%, 71%)) are still bigger than those of the pixels in our very dark background, so the result of blending with it is still hotpink.

Where the lighten approach breaks

However, if we were to choose something like a background: red for our p, this approach also fails, as the green and blue channel values are 0 for red (rgb(100%, 0%, 0%)) - that is, lower than even the very low values we have for our dark green background image on the body. You can see below how the result of blending wouldn't be rgb(100%, 0%, 0%) anymore:

max(1, .04) = 1
max(0, .13) = .13
max(0, .06) = .06

And here's a screenshot of what it would look like:

enter image description here

Ugh...

And it would only be worse if the layer underneath, the background-image on the body, wasn't as dark. Take the first image we used in our examples - not even the hotpink background that worked for our p with the darker body background image would still work out now.

enter image description here

At the other lightness end

Let's also see a pure CSS way for the case where the background-image on the body is a very bright one and the background of our p is a very dark one.

Since we want to see through to the background of body in the area occupied by the actual text letters, we can choose any initial RGB value we want for them and, in this case, we choose white.

So in the case when we start with background: black on our p, we may use the multiply blend mode.

html, body { display: grid }

html { height: 100% }

body {
  background: url(https://images.unsplash.com/photo-1489069313310-63735a4f3010?w=1400) 50%/ cover
}

p {
  place-self: center;
  padding: 0 0.125em;
  width: min-content;
  background: #000;
  color: #fff;
  font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
  text-align: center;
  mix-blend-mode: multiply
}
<p contenteditable>Hello, world!</p>

How the multiply blend mode works

In the case of the multiply blend mode, if C₁ is a generic channel (R,G, or B, doesn't matter) of a top layer pixel and C₀ is a generic channel value of the corresponding pixel in the bottom layer, the formula is:

C₁·C₀

Let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a white one, corresponding to the actual text (that is rgb(100%, 100%, 100%), all three channel values maxed out at 0%). Let's say the corresponding pixel in the body background image is rgb(45%, 77%, 80%) - a sort of light cyan.

Then the resulting RGB channels of that pixel after the multiply blend mode operation are:

1·.45 = .45
1·.77 = .77
1·.80 = .80

That is, the exact same channel values as for the pixel in the body background image!

In general, when a pixel in one of the two layers is white and we use the multiply blend mode, then the result of blending is the RGB value of the pixel in the other layer (not the one with the white pixel).

This is because when we have a white pixel, all its three channels are maxed out at 100%, 1 in decimal representation, which makes the entire product equal to the other factor.

Now let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a black one, corresponding to the background (that is rgb(100%, 100%, 100%), all three channel values maxed out at 100%). Let's say the corresponding pixel in the background image the same light cyan rgb(45%, 77%, 80%).

Then the resulting RGB channels of that pixel after the multiply blend mode operation are:

0·.45 = 0
0·.77 = 0
0·.80 = 0

That is, rgb(0%, 0%, 0%) or black!

In general, when a pixel in one of the two layers is black and we use the multiply blend mode, then the result of blending is also black.

This is because when we have a black pixel, all its three channels are at 0%, 0 in decimal representation, so either the C₁ or the C₀ (depending in which of the two layers the pixel is black) is 0, thus making the entire product/ end result 0 (or 0%). And all three channel values at 0%, that means black.

Where the multiply approach breaks

For any background of our p that's not fully black, this approach fails.

Let's say our p has an indigo background (rgb(29%, 0%, 51%)), something that's still very dark to contrast with a bright body background image. Replacing in our formula (and considering the same rgb(45%, 77%, 80%) value for the corresponding background pixel), we have the following for the three channels:

.29·.45 = .1305
0·.77 = 0
.51·.80 = .408

The result is rgb(13.05%, 0%, 40.8%), which is different from the indigo background we started with, rgb(29%, 0%, 51%). They're both dark, but they're different and this difference depends on the RGB channels of the body background underneath.

The screenshot below illustrates this problem we run into if we want any background that's not fully black for our p when using the multiply blend mode:

enter image description here

The lighter the background of the p, the more obvious the problem is.

darken - maybe another acceptable option?

Now if the body background image is very bright (just bright, but around the same channel(s) as the background we want for our p might work too), we might get away with changing the blend mode to darken.

For example, something like below, where the body background image is mostly just light orange-pink, while the we've set background: maroon for our p:

html, body { display: grid }

html { height: 100% }

body {
  background: url(https://images.unsplash.com/photo-1516464278939-6c47180c46eb?w=1400) 50%/ cover
}

p {
  place-self: center;
  padding: 0 0.125em;
  width: min-content;
  background: maroon;
  color: #fff;
  font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
  text-align: center;
  mix-blend-mode: darken
}
<p contenteditable>Hello, world!</p>

How the darken blend mode works

In the case of the darken blend mode, if C₁ is a generic channel (R,G, or B, again, it doesn't matter, the formula is the same) of a top layer pixel and C₀ is a generic channel value of the corresponding pixel in the bottom layer, the formula is as follows:

min(C₁, C₀)

Again, a very simple formula.

Let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a white one, corresponding to the actual text (that is rgb(100%, 100%, 100%), all three channel values maxed out at 100%). Let's say the corresponding pixel in the body background image is rgb(100%, 49%, 44%) - a kind of light salmon.

Then the resulting RGB channels of that pixel after the darken blend mode operation are:

min(1, 1) = 1
min(1, .49) = .49
min(1, .44) = .44

Again that's the exact same channel values as for the pixel in the body background image!

This is because when we have a white pixel, all its three channels are at maxed out 100%, 1 in decimal representation. And any other channel value, which is always in the [0, 1] range is at least as small as 1 - any number from 0 to 1 is smaller or equal to 1. So the minimum between 1 and a value in the [0, 1] interval is always equal to the value in the [0, 1] interval.

Now let's say the pixel at the intersection between the ith column of pixels and the jth row of pixels of the p element is a maroon one, corresponding to the background of the p (that is rgb(50%, 0%, 0%)). Let's say the corresponding pixel in the background image is the same light salmon - rgb(100%, 49%, 44%).

Then the resulting RGB channels of that pixel after the darken blend mode operation are:

min(.5, 1) = .5
min(0, .49) = 0
min(0, .44) = 0

That's the same as the maroon we chose as the background of the p!

Where the darken approach breaks

However, if we were to choose something like a background: indigo for our p, this approach also fails, as the blue channel value is 51% for indigo (rgb(29%, 0%, 51%)) - that is, lower than even the very low values we have for our dark green background image on the body. You can see below how the result of blending wouldn't be rgb(29%, 0%, 51%) anymore:

min(.29, 1) = .29
min(0, .49) = 0
min(.51, .44) = .44

And here's a screenshot of what it would look like - even worse than when the same background: indigo on the p fails with the multiply blend mode:

enter image description here

Pure CSS: background-clip: text

This has already been mentioned, so I'd normally avoid going into it again, especially since it's not a solution I would use given its scroll complications, but so much has changed in the decade that passed since the previous answers!

background-clip: text is now standard, supported by all browsers, doesn't require the -webkit- prefix anymore in any of the modern browsers (or maybe only needs it on mobile, where I haven't tested, as I myself don't have a smartphone and in my environment don't have access to a smartphone that's less than 10 years old and running old versions of everything) and can be used in the shorthand.

html, body { display: grid }

html { height: 100% }

body { background: url('https://images.unsplash.com/photo-1489069313310-63735a4f3010?w=1400') 50%/ cover fixed }

p {
    place-self: center;
    position: relative;
    padding: 0 .125em;
    width: min-content;
    background: black 50%/ cover text fixed;
    background-image: inherit;
    color: transparent;
    font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
    text-align: center;
    
    &::before {
        position: absolute;
        inset: 0;
        z-index: -1;
        background: indigo;
        content: ''
    }
}
<p contenteditable>Hello, world</p>

No need for any workarounds to emulate it in other browsers, such as -moz-element() in Firefox (prefixed version of the standard element() version, which Chromium browsers haven't implemented and probably won't implement due to security concerns).

The problem with this is it requires to set the same background-image on both the body and the p element and the images need to be aligned. Something that's easily achievable with background-attachment: fixed, but this means the background cannot scroll with the page. May be totally fine in some cases, but not in others.

The even more annoying problem with this is that we need to use up a pseudo-element due to a Firefox bug soon old enough to go to school.

Without this bug, this little bit of code would do it:

body { background: url(myimage.jpg) 50%/ cover fixed }

p {
    background: 
        url(myimage.jpg) 50%/ cover text fixed, 
        linear-gradient(indigo 0 0);
    color: transparent
}

Note the background-clip: text approach means we run into problems if we need to scroll.

Using an SVG filter: create real text transparency

The idea is the following: if we don't want our background to be black, then we make our text black (that is, having all three RGB channels zeroed rgb(0, 0, 0)) and we apply a filter that gives us a result whose alpha channel is computed out of the RGBA channels of its input as follows:

v·R + v·G + v·B + 0·A + 0 = v·(R + G + B) + 0 = v·(R + G + B)

This means the alpha of the result is 0 wherever the RGB channels of the input are all 0, so wherever the pixels of the input are rgb(0, 0, 0) (that is black). In our case, that's the text.

html, body { display: grid }

html { height: 100% }

body {
    background: url('https://images.unsplash.com/photo-1700607687506-5149877683e8?w=1400') 50%/ cover fixed
}

svg[width='0'][height='0'] { position: fixed }

p {
    place-self: center;
    position: relative;
    padding: 0 .125em;
    width: min-content;
    background: white;
    font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
    text-align: center;
    filter: url(#punch-through)
}
<svg width='0' height='0'>
  <filter id='punch-through'>
    <feColorMatrix values=' 1   0   0 0 0
                            0   1   0 0 0
                            0   0   1 0 0
                          999 999 999 0 0'/>
  </filter>
</svg>

<p contenteditable>Hello, world!</p>

Now you may be wondering what's up with those values in that formula, what's the deal with the matrix, how are they connected...

v v v 0 0

are the values on the fourth (final) row of the matrix values for feColorMatrix. v is a large value, let's throw in a random 999, in order to ensure the result alpha outside of the fully black areas (that is, outside the text) is always ≥ 1 (which means fully opaque).

The other three rows give us the RGB channels and those we just want to leave unchanged. The red channel of the output is just 1 multiplied with red channel of the input plus a bunch of zeroes and the same is valid for the green and blue channels.

1·R + 0·G + 0·B + 0·A + 0 = R + 0 + 0 + 0 + 0 = R
0·R + 1·G + 0·B + 0·A + 0 = 0 + G + 0 + 0 + 0 = G
0·R + 0·G + 1·B + 0·A + 0 = 0 + 0 + B + 0 + 0 = B

This means the first three matrix rows are:

1 0 0 0 0
0 1 0 0 0
0 0 1 0 0

(1 diagonally and 0 elsewhere)

Now... depending on how picky you are, you might be happy with this result... or not!

I'm not happy with it.

There's still some black along the edges and it looks kinda rough.

Fortunately, in this particular case, the background of our p is white, which means we're maxing out all three RGB channels at 100%, that's our input for them, that's what we want the output to be, no need to even bother with even something as simple as multiplying the input channel values with 1 anymore!

Just drop the decimal representation (1 for 100%) on the last column of the matrix and zero everything else for the first three rows, which basically means this is what gets computed (R, G, B and A are all 1 and that's something we know beforehand):

0·R + 0·G + 0·B + 0·A + 0 = 0·1 + 0·1 + 0·1 + 0·1 + 0 = 0 + 0 + 0 + 0 + 1 = 1
0·R + 0·G + 0·B + 0·A + 0 = 0·1 + 0·1 + 0·1 + 0·1 + 0 = 0 + 0 + 0 + 0 + 1 = 1
0·R + 0·G + 0·B + 0·A + 0 = 0·1 + 0·1 + 0·1 + 0·1 + 0 = 0 + 0 + 0 + 0 + 1 = 1

As for the final row, the one giving us the alpha of the result, it's the same as before, we just make v equal to 1/3 since we know the background RGB channels in this case are maxed out at 100% (1), so multiplying them with 1 and then adding up the products won't give us a subunitary value for the result alpha (1 and greater means fully opaque, in the (0, 1) interval means semitransparent and 0 means fully transparent) outside the actual text, which is black, so all channels are 0.

So for the white background, the alpha computes to:

v·R + v·G + v·B + 0·A + 0 = ⅓·1 + ⅓·1 + ⅓·1 + 0·1 + 0 = ⅓ + ⅓ + ⅓ + 0 + 0 = 3/3 = 1

while for the black text, the alpha computes to:

v·R + v·G + v·B + 0·A + 0 = ⅓·0 + ⅓·0 + ⅓·0 + 0·1 + 0 = 0 + 0 + 0 + 0 + 0 = 0

Now... at the edges of the text, we have some pixels that are neither fully black nor fully white and those give us some semitransparency there to smoothen out the edges, make them look nice.

html, body { display: grid }

html { height: 100% }

body {
    background: url('https://images.unsplash.com/photo-1700607687506-5149877683e8?w=1400') 50%/ cover fixed
}

svg[width='0'][height='0'] { position: fixed }

p {
    place-self: center;
    position: relative;
    padding: 0 .125em;
    width: min-content;
    background: white;
    font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
    text-align: center;
    filter: url(#punch-through)
}
<svg width='0' height='0'>
  <filter id='punch-through'>
    <feColorMatrix values=' 0   0   0 0 1
                            0   0   0 0 1
                            0   0   0 0 1
                          .33 .33 .33 0 0'/>
  </filter>
</svg>

<p contenteditable>Hello, world!</p>

But what if we don't want a white background? No problem, we can easily set any other solid background in the SVG filter!

We just flood the entire filter area with the desired background and give it the alpha of the punched through white layer.

html, body { display: grid }

html { height: 100% }

body {
    background: 
        url('https://images.unsplash.com/photo-1700607687506-5149877683e8?w=1400') 
            50%/ cover fixed
}

svg[width='0'][height='0'] { position: fixed }

p {
    place-self: center;
    position: relative;
    padding: 0 .125em;
    width: min-content;
    background: white;
    font: 900 clamp(1.25em, 25vmin, 25em)/ 1.125 sans-serif;
    text-align: center;
    filter: url(#punch-through)
}
<svg width='0' height='0'>
  <filter id='punch-through'>
    <feColorMatrix values=' 0   0   0 0 1
                            0   0   0 0 1
                            0   0   0 0 1
                          .33 .33 .33 0 0' result='punched'/>
    <feFlood flood-color='darkslategrey'/>
    <feComposite operator='in' in2='punched'/>
  </filter>
</svg>

<p contenteditable>Hello, world!</p>

The semitransparent #2f4f4fa4 as a flood-color works just as well.

We can even replace the white background on our p with something like:

repeating-linear-gradient(45deg, transparent 0 4px, white 0 2rem)

for a background on our p that has fully opaque and fully transparent parts - try it out!

Apache answered 21/12, 2023 at 17:9 Comment(0)
B
10

Does it have to be dynamic? The only way to do that is with an image with transparency (GIF or, better, PNG).

I'm not sure if this is what you want, but will explain it anyway.

Situation: you have a non plain background that you want to bee seen through your text.

Solution: no CSS is coming to the rescue for this. You'll have to use your trusty image editor to create a layer with text, and another layer that will be the negative of your text

This could allow you to have some interesting effects, but if you want it to be dynamic, you'll have to generate the images on the fly serverside.

This kind of trickery is currently impossible with pure CSS (might be possible with Javascript).


Edit

Seeing Paul's find on webkit got me thinking on how to fake that behavior in Firefox, Opera and IE. So far I've had good luck using the canvas element on Firefox, and I'm trying to find some behavior in filter:progid:DXImageTransform.Microsoft.

So far with canvas, this is what I did

<html>
<body>
<canvas id="c" width="150" height="150">
</canvas>
<script>
ctx = document.getElementById("c").getContext("2d");
// draw rectangle filling the canvas element
ctx.fillStyle = "red";
ctx.fillRect(0,0,150,150);

// set composite property
ctx.globalCompositeOperation = 'destination-out'; 
// the text to be added now will "crop out" the red rectangle
ctx.strokeText("Cropping the", 10, 20);  
ctx.strokeText("red rectangle", 10, 40);  

</script>
</body>
</html>

by using a detination-out compositing and drawing text on the canvas.

Barthol answered 18/2, 2010 at 14:49 Comment(3)
Oh! Now I get what mnml was asking. Sorry, seems obvious now.Kurgan
For the record, I think this is called "punch-through" or "knockout" transparency - see e.g. help.adobe.com/en_US/photoshop/cs/using/… (and https://mcmap.net/q/909587/-how-do-i-make-punch-through-transparent-text-in-html-css/20578).Kurgan
Eugh, all your images no longer work. I don't suppose you can re-upload any replacements using the Stack Exchange image uploader? In the meantime, I've edited the images out, but tried to keep the value of the post... obviously, feel free to edit anything back in if I've somehow invalidated your points (and sorry if I did!).Ballard
K
6

I’m not exactly clear what you’re asking (100% transparency means that something’s invisible, and invisible text isn’t generally a great idea), but in general:

  1. The CSS opacity property applies to an entire element, not just its text. So if you have this HTML:

    <div class="opacity-50">
        This is a div with a background colour and 50% opacity
    </div>
    

    And this CSS:

    .opacity-50 {
        background: #ccc;
        color: #000;
        opacity: 0.5;
    }
    

    Then both its background and its text will have 50% opacity.

  2. rgba colour values allow you to specify semi-transparent colours. So, if you have this HTML:

    <div class="text-opacity-50">
        This is a div with semi-transparent text
    </div>
    

    And this CSS:

    .text-opacity-50 {
        background: #ccc;
        color: rgba(0,0,0,0.5);
    }
    

    Then only its text will have 50% opacity.

I think rgba colour values are supported by slightly fewer browses than opacity.

Kurgan answered 18/2, 2010 at 14:21 Comment(3)
What I meant was: [transparent-text/InsideA/colored-Div]-[colored-background] thanks for the explanationWalters
I’m not super-clear on what that combination of punctuation means either, to be honest.Kurgan
I was just trying to explain the "layers" of what I want to doWalters
K
4

Ah — if you’re talking about “punch-through” transparency, no, CSS doesn’t do this.

Except for WebKit (the rendering engine in Safari and Chrome), which has a totally custom, made-up-by-Dave-Hyatt, not-even-in-CSS-3 property value, -webkit-background-clip: text;.

No other browser other than Safari and Chrome supports it.

Kurgan answered 19/2, 2010 at 0:49 Comment(6)
+1 Interesting find! Confirmed on Chrome. No other browser (not even Konqueror) supports it right now. I imagine that you can fake it with some trickery on IE, and on Firefox it could be done with javascript (I know I've seen something like that somewhere). As for Opera...Distill
Very nice, I would use that if firefox was using webkit. I'm going to stick with voyager's answer for now I guess.Walters
Yeah, WebKit is the place where CSS innovation is happening fastest. (Although most of their innovations do seem to be Mac OS X internals being exposed by CSS, which I guess is sort of cheating — Mozilla doesn’t sell an operating system, so they don’t have a similar body of code to call on.)Kurgan
Oh — could you fake this with JavaScript in Firefox? I can’t imagine how.Kurgan
On IE, I imagine that there is something in filter:progid:DXImageTransform.Microsoft that you could use. On FF you might be able to do some Canvas or SVG trickery.Distill
For FF: developer.mozilla.org/en/Drawing_text_using_a_canvas + developer.mozilla.org/en/Canvas_tutorial/Compositing (source-out compositing)Distill
Z
1

You can spent the time to make your own font with InkScape and IcoMoon and make a negative knocked out font, then your letters can be see trough! I used this technique for some see trough Icons.

Zacharia answered 4/12, 2013 at 9:34 Comment(0)
I
0

Why not try to set the DIV's background image to a completely transparent GIF?

http://www.pageresource.com/dhtml/csstut9.htm

Implore answered 8/7, 2011 at 10:24 Comment(0)
I
0

Using a .png without background is a good method. In Photoshop you can save for the web.

or in css:

#div
{
    background:transparent;
    color:whatever;

}
Incipient answered 27/2, 2013 at 19:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.