The Power of CSS Masks

December 2023 became a significant date in the history of the development of CSS properties mask: all modern browsers in their latest versions have provided full support for it, now without using their vendor prefixes. This means that this property has firmly and permanently entered the life of every front-end developer. All that remains is for front-end developers to accept it into their lives and stop being afraid of it!

In the article I will briefly recall the main theoretical ideas of the property and talk in detail about real examples of use based on development experience Taiga UI.

What is a mask in CSS

Historically, the term “masking” has been used very widely in different areas of life and with radically different meanings. The mask discussed in the article came to the web from the world of design. There, masking is a very popular technique with which you can hide or cut out part of an image of any shape. Let's look at a very simplified example.

There is a beautiful picture generated by a neural network through a prompt, which included the words “taiga”, “sunset” and “winter”.

https://habr.com/ru/companies/tbank/articles/853042/taiga-winter-sunset.jpeg

taiga-winter-sunset.jpeg

And there is this wonderful logo of our open source product Taiga UI:

taiga-logo.svg

taiga-logo.svg

It is important to emphasize that in the last image, all areas whose color is other than orange are a transparent background. Here sources pictures. This idea is key to everything that will happen next.

We create the most minimalistic web application. Inside body markup we place a single tag imgV src which we feed the picture generated earlier by the neural network:

<img src="https://habr.com/ru/companies/tbank/articles/853042/taiga-winter-sunset.jpeg" />

And add the following to the contents of the connected CSS file:

body {
   background: mediumpurple;
}

img {
   mask-image: url(taiga-logo.svg);
   mask-repeat: no-repeat;
   mask-size: auto 100%;
   mask-position: center;
}

We get this beauty:

Let's start with the main property mask-image. We fed it into svg picture. The mask image has only two color areas: a transparent background and an orange logo in the shape of a Christmas tree.

The browser took everything non-transparent from the mask image – all this continued to be displayed on the original sunset image. And the rest, transparent, was simply cut out – as in children's appliqués with scissors. Moreover, the entire disappeared part of the picture was cut out for real: this area was filled with a purple background, which we hung on body-tag.

Let's look at the remaining properties. As they say in many romance novels, you'll know it when you've lost it. Let's do just that: let's see what would happen if I removed all properties except mask-image:

img {
   mask-image: url(taiga-logo.svg);
}

We got too many Christmas trees because they put a tiny mask image on top of a huge sunset picture. The original dimensions of the svg logo are only 68 × 60 pixels. By default, the browser tries to fit as many masks as possible, if possible. To configure this behavior, we need the property mask-repeat. Let's put it back in place:

img {
   mask-image: url(taiga-logo.svg);
   mask-repeat: no-repeat;
}

The updated situation looks like this:

The repeating Christmas trees have been removed, but the size is clearly not the same. Getting to know the property mask-sizewhich takes two values ​​- the width and height of the mask image. In this case, we want our logo to be full-length, and the width of the image to be stretched, maintaining the original proportions.

Our image is close to the treasured one, but for now it is nailed to the left edge. Armed with property mask-positionwhich can take up to two arguments: shift along the horizontal and vertical axis. There can be only one argument, then it is applied to both axes at once.

You can experiment with the examples just described in the StackBlitz example:

We have covered the minimum base that you need to know about CSS masks. If, as you continue reading, you encounter difficulties in interpreting the syntax or basic concepts of masking, then my personal recommendation is to first take a look at the article by Ahmad Shadid “CSS masking”. Now let's move on to real cases!

Fade

Too many letters is a problem that we constantly encounter on the web. Even novice specialists know perfectly well how to solve it using CSS properties text-overflow. It is enough to send it elipsis – and the overflow of content will be “cut off” with an ellipsis. This is the base.

But then the designer comes and says that now we don’t want to see ellipses in the design system. We want a smooth fading of overcrowded content – as in the illustration below:

For a block that contains only one-line text, the problem can be solved very simply:

.fade {
   mask-image: linear-gradient(to right, black 80%, transparent 90%);
}

We told the browser to let it act as a mask linear gradientwhich from left to right the first 80% is painted black, and the last 10% is completely transparent, and 10% between the completely black zone and the completely transparent zone (80-90% gap) let there be a smooth transition with a gradual fading of black into a transparent color.

Currently there are no convenient tools for debugging CSS masks in DevTools. But there is a life hack: when developing, temporarily change mask-image on background-image. This will allow you to visually see what the mask is: everything opaque remains, everything transparent is cut out.

I won’t impress many people with the solution to this problem; it is found in almost every educational material about CSS masks. But let's complicate the task.

The designer comes with new “happy” news that we want to display text fading not only for single-line content, but also for multi-line content. For example, only the first two lines of a long text are fully visible, let the third gradually fade out, and let all subsequent lines be completely hidden.

The previous solution will no longer work here. A multi-layer mask will do. Each element can have more than one mask, but several layers at once. The syntax is simple: we transfer the characteristics of each new layer to all studied properties, separated by commas:

.multi-line-fade {
    height: 3lh;
    overflow-y: hidden;
    mask-image:
        linear-gradient(black, black),
        linear-gradient(to right, transparent 80%, black 90%);
    mask-position:
        0 0,
        bottom right;
    mask-size:
        auto,
        100% 1lh;
    mask-repeat: no-repeat;
}

The first two style properties say that the container with the text should not occupy a height of more than three lines, and let everything else be hidden. After in property mask-image we listed the two layers of our mask separated by commas.

The first layer is a gradient that goes from black to black, that is, in fact, solid black throughout its entire interval. Just pass the word black won't work.

The second layer is almost the same gradient that we used for the single-line text in the previous solution, but we swapped the colors black And transparent. Through properties mask-position And mask-size we determine where and how the layers of the mask will be located: let the first layer occupy the entire space of the first three lines, and the second – only the last visible third line.

While this still won't work, the first mask layer will simply overlap all of the first three lines, rendering the second layer completely useless. You want the contents of the second layer to be excluded from the first layer.

Let me introduce you to another possible configuration of CSS masks – mask-composite. It takes on many possible values. In short, its essence is to configure how layers of masks will be combined with each other. Default (value add) they overlap each other, expanding the coverage area of ​​the opaque area of ​​the mask.

In our case, this default behavior is not suitable, but the property is suitable excludewhose documentation states: “Nonoverlapping regions are merged.” That is why we made the second layer of the mask this way: its first 80% is transparent, which means it does not intersect with the first layer. The rest of the content has translucent content – it is this that will intersect with the first layer and partially exclude this area from the final result of the “collaboration” of the two layers. We get the final solution for content overflow in multi-line text:

.multi-line-fade {
    height: 3lh;
    overflow-y: hidden;
    mask-image:
        linear-gradient(black, black),
        linear-gradient(to right, transparent 80%, black 90%);
    mask-position:
        0 0,
        bottom right;
    mask-size:
        auto,
        100% 1lh;
    mask-repeat: no-repeat;
    mask-composite: exclude;
}

You can see the final solutions obtained as a StackBlitz example using this link:

And if you want to be inspired by an even more complex technical solution (but even more flexible), I invite you to take a look at the taiga directive Fade.

Sensitive

You are given a new task – to develop a tool that will visually hide any part of the content from the user. Here it is in this form:

In T-Bank applications, this feature is actively used so that the user can hide his sensitive data – the value of his balance or bank account when recording a screen or showing something personally to his friends. You might have seen a similar feature in telegram channels in the text or spoiler image.

Let's create a mask image. We need a simple svg that consists of chaotic squares of varying degrees of transparency:

<svg
   width="360"
   height="48"
   preserveAspectRatio="none"
   fill="black"
   xmlns="http://www.w3.org/2000/svg"
>
   <rect opacity="0.2" width="24" height="24"/>
   <rect opacity="0.2" x="336" y="24" width="24" height="24"/>
   <rect opacity="0.35" x="120" y="24" width="24" height="24"/>
   <!-- [...] -->
   <rect opacity="0.2" x="264" y="0" width="24" height="24"/>
   <rect opacity="0.32" x="168" y="0" width="24" height="24"/>
</svg>

There are many options for creating such a mask. You can generate such tags through the IDE rectyou can ask the designer to create this in Figma, or you can take our ready-made solution. The main thing is that the final version looks like something similar. It is important to use degrees of transparency, not shades of gray!

The next trick is that nothing prevents you from inlining the resulting svg inside a CSS property:

.sensitive {
   background: currentColor;
   mask-image: url('data:image/svg+xml,<svg width="360" ...>...</svg>');
   mask-size: auto 100%;
}

When using this technique in different places, the browser will not load the svg file again; it will take care of caching on its own.

We intentionally set the property background: currentColor — the translucent squares of the mask will take on the color of the text they cover.

And again success! I invite you to study the sources of our taiga component Sensitive and I’m attaching a StackBlitz example for experimentation:

Minimalistic Checkbox

It's time to learn how to create custom checkboxes without creating unnecessary HTML nesting, but only by resorting to the power of CSS masks.

During the many years of development of Taiga UI, we always tried not to create unnecessary nesting of markup unnecessarily. Neglecting this rule complicates the customization of components, which has to be compensated for by inflating the number of public CSS variables.

First, let's take the native one <input type="checkbox" /> and disable all built-in browser customization via appearance: none. Afterwards, let’s customize the appearance of the “box” from the checkbox:

input[type="checkbox"] {
   appearance: none;
   cursor: pointer;
   width: 2rem;
   height: 2rem;
   position: relative;
   overflow: hidden;
   box-shadow: inset 0 0 0 0.125rem lightgray;
   border-radius: 0.5rem;
}

Let's add behavior that when the state is selected, the checkbox is smoothly filled with color. This is where the pseudo-class comes in handy :checked:

input[type="checkbox"] {
   /* [...] Портянка уже ранее объявленных свойств */
   background: transparent;
   transition: background-color 0.3s;
}

input[type="checkbox"]:checked {
   background: lavender;
}

All that remains is to add a checkmark. Let's use a pseudo element :after and my knowledge of CSS masking:

input[type="checkbox"]::after {
   content: '';
   position: absolute;
   inset: 0;
   background: #333;
   mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M14 5L7 12L3 8C3 8 4 7 5 7.5L7 9.5L11.5 5C11.5 5 13 4 14 5Z"/></svg>');
   transform: scale(0);
   transition: transform 0.3s;
}

input[type="checkbox"]:checked::after {
   transform: none;
}

position:absolute + inset: 0 allowed us to position the pseudo element :after the entire width and height of the host. Then we fill the entire pseudo-element with a shade of black. And through mask-image cut out the checkmark shape using a simple inline icon. All other lines are responsible for the smooth animation of the appearance and disappearance of the checkmark.

See the result in action:

Taiga implementation of the component checkbox is based on the solution described. But at the same time, it contains even more interesting techniques that are beyond the scope of this article. Be sure to check it out at your leisure!

I don't say goodbye

I hope I was able to convince you that CSS masking is a very interesting and useful tool in the arsenal of any front-end developer. His deep knowledge can work real miracles!

The cases presented in this article are not the only CSS masking tricks that you can find in our component library Taiga UI. But I don’t want to bore you with too long reading at once. Yes, and I need a break to collect my thoughts to explain more complex examples.

In continuation, we will look at the rest of our examples based on CSS masking: how and why we layered radial gradients in ProgressSegmentedtricks for working with the new component Iconand also consider the complex component Switch.

See you soon!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *