ARTICLE AD BOX
Here's another method using SVG, this time a super simple SVG filter.
This is very flexible, it doesn't have the limitations of SVG text, it doesn't require aligning backgrounds like the background-clip: text method does, it doesn't have any limitations on what background works around the text like the mix-blend-mode method does... and it has very good browser support!
EDIT A few hours after answering this, I just had another idea which doesn't require a different filter for each element with a different background.
In the CSS, we set the desired background, only to a subunitary alpha, but still close to 1 - we don't want to go to low, as that might alter the background. The color property gets the same as the background, only fully opaque. We cannot leave it with the default black value otherwise we'll have black edges around the letter holes.
The SVG filter maps the input alpha interval [0, 1] to [20, 0] (if the very low background alpha is .95, then we have 1/(1 - .95) = 20). This means an alpha of 0 (fully transparent) gets mapped to 20 (which then gets clamped to the [0, 1] interval) and the actual background alpha gets mapped to 1 (fully opaque), while the fully opaque (1) text alpha gets mapped to 0 (fully transparent).
Most CSS styles are just prettifying and layout and you can change those as you wish. All that really matters is the filter.
Let's deconstruct the feColorMatrix primitive.
The three numbers on the final column give us the RGB channels of the final background around the transparent text.
In this case, we have the red channel (1st row) maxed out at 100% (1 in the decimal representation we use in the matrix), the green channel (2nd row) about half way at 53% (.53 in decimal representation) and the blue channel contribution is pretty much insignificant at 3% (.03 in decimal representation).
If we wanted to have a deep blue instead, let's say rgb(0, 191, 255), then the values we'd need to put on the last column on those three rows would compute as follows:
0/255 = 0 191/255 = .75 255/255 = 1Here's an interactive demo illustrating pretty much the same, but without the alpha inversion we're discussing in a moment.
The first three rows (each with 5 values) of the matrix give us the RGB channel, but what about the alpha channel? Yup, that's given by the final row!
We don't want the alpha to depend on any of the RGB channels, so we zero the first three values on this final row.
What we want is to make our div transparent in the area where it is initially opaque (the text area) and opaque everywhere it's initially transparent (everywhere around the text).
Basically, our output alpha should be 1 (constant) minus our input alpha - that is, we're inverting the alpha.
The constant value is the final value of the final row (1) and the input alpha coefficient is the fourth value on the final row (-1).
If we are to compute it, our output alpha is:
-1*a + 1 = 1 - aWhere a is our input alpha. If we wanted to reduce the alpha of the area outside the text to let's say .8, then our output alpha should be:
.8*(1 - a)This means we need to change the values on the final matrix row to -.8 (for the input alpha coefficient) and .8 (for the constant/ final value).
Don't want to set RGB like that
Maybe you don't like matrices. Maybe you don't want to compute the decimal representation of percentage RGB values. There's still hope for you!
feComponentTransfer allows us to manipulate individual RGBA channels. In our case, via feFuncA, we mess just with the alpha channel. Using the table type of transfer function, we map the [0, 1] interval to [1, 0]. The ends of the interval are the values given in tableValues (1 0 in this case), in that order.
Basically, where the input alpha is 1 (text area), the output alpha is 0 and where the input alpha is 0 (around the text), the output alpha is 1.
If we want the background to be semitransparent, we map the [0, 1] interval to [a, 0], where a is the desired alpha (.5 or whatever).
feComponentTransfer is actually more complex in the general case, so if you're interested, further reading:
one twoNow this just gives us a black background around the text because transparent is rgba(0, 0, 0, 0) and all we've done here was to flip that alpha channel from 0 to 1, which gave us rgba(0, 0, 0, 1) around the text (that is, black).

So we save this result as inverted (in the result attribute of feComponentTransfer, not feFuncA!) and we want to paint it.
We first flood the entire filter area with a cool background (which can be stored in a custom property like --cool-bg). Note that we don't even need to set the flood-color attribute there. This one rare filter primitive attribute we can set in the CSS. Not sure it makes sense here, but we can do this:
feFlood { flood-color: var(--cool-bg) }Note that --cool-bg needs to be set "upstream" from feFlood, we cannot set it on the div, which is on another branch.
Now we use this cool background to paint the non-transparent areas in (ahem, the in operator of feComposite) the previous inverted result.
feComposite takes two inputs. Both are by default the result of the previous filter primitive (in our case, feFlood), but we want the second one (in2) to be the result of the earlier feComponentTransfer (inverted).
Want an image background
The image background version is almost exactly the same as the previous one, we just use a feImage instead of feFlood, that's all!
feImage has the image URL and a preserveAspectRatio, which works simlarly to when set on the svg element (good article on that).
We can also make this image semitransparent by replacing 1 with the desired subunitary alpha for the tableValues attribute.
Want a programmatic gradient background
This is a bit more complicated and requires a pseudo-element (because of a Firefox bug) and the gradients we can have are kinda limited (because of another Firefox bug). But it's still doable!
The actual element holds the text and the pseudo-element holds the gradient type, direction, stop positions... you can try changing the gradient direction, the type of gradient...
We blend the two together using the lighten blend mode. This works on a per pixel , per channel basis. We have two layers and we take each pair of corresponding pixels from them and then, for each channel, the resulting value is the maximum channel value of the two.
Our text is blue, so strictly on the blue channel, the red and green channels are always zero. The gradient goes from red to black, so it's strictly on the red channel, the other two channels are always zero.
This means we can extract the text and the gradient separately in the filter using feColorMatrix (interactive demo illustrating this).
We extract the red channel - this gives us a black to transparent gradient.
We paint it in --c0 using feFlood & feComposite.
We flood the entire filter area with --c1.
We place on top of it (feBlend with the default blend mode normal) the gradient from solid --c0 to transparent --c0. We now have the gradient background.
We extract the blue channel - this gives us the text.
We subtract the text alpha out of the gradient background alpha.
A couple more notes:
Since the svg element isn't used to display any graphics, it's only there to host the filter, we zero both its dimensions and then, in the CSS, we take it out of the document flow (by giving it position: fixed). Note the attribute selectors: we don't want to take out of the document flow any other svg elements (that may be used to display icons or illustrations). The filter element needs the color-interpolation-filters attribute set to sRGB, otherwise the RGB value of the background around the text won't look as expected in Chrome and Firefox. I don't really understand the linearRGB/ sRGB stuff, so just know we need that attribute to be set to sRGB there.