Мощь CSS-масок
- пятница, 25 октября 2024 г. в 00:00:07
Декабрь 2023 года стал значимой датой в истории развития CSS-свойства mask: все современные браузеры в своих последних версиях обеспечили его полную поддержку, теперь без использования своих вендорных префиксов. А это означает, что данное свойство прочно и надолго вошло в жизнь каждого фронтенд-разработчика. Осталось лишь фронтенд-разработчикам принять его в свою жизнь и перестать его бояться!
В статье я кратко напомню основные теоретические идеи свойства и подробно расскажу о реальных примерах использования на основании опыта разработки Taiga UI.
Исторически сложилось, что термин «маскирование» очень широко применяется в разных сферах жизни и с кардинально разными значениями. Маска, о которой речь в статье, пришла в веб из мира дизайна. Там маскирование — весьма популярная техника, с помощью которой можно скрыть или вырезать часть изображения произвольной формы. Рассмотрим на очень упрощенном примере.
Есть красивая картинка, сгенерированная нейросетью через промпт, в котором фигурировали слова «тайга», «закат» и «зима».
И есть вот такое прекрасное лого нашего open source продукта Taiga UI:
Важно подчеркнуть, что на последнем изображении все зоны, цвет которых отличен от оранжевого, — это прозрачный фон. Вот исходники картинки. Эта мысль ключевая во всем том, что будет происходить далее.
Верстаем самое минималистичное веб-приложение. Внутри body
разметки помещаем единственный тег img
, в src
которого кормим картинку, сгенерированную ранее нейросетью:
<img src="taiga-winter-sunset.jpeg" />
А в содержимое подключенного CSS-файла складываем следующее:
body {
background: mediumpurple;
}
img {
mask-image: url(taiga-logo.svg);
mask-repeat: no-repeat;
mask-size: auto 100%;
mask-position: center;
}
Получаем вот такую красоту:
Начнем с главного свойства mask-image
. В него мы скормили svg-картинку. У картинки-маски всего два цветовых участка: прозрачный фон и оранжевое лого в форме елочки.
Браузер взял все НЕпрозрачное из картинки-маски — все это продолжило отображаться на изначальной картинке с закатом. А остальное прозрачное просто вырезалось — как в детских аппликациях ножницами. Причем вся исчезнувшая часть картинки вырезана по-настоящему: эта область залилась фиолетовым фоном, который мы навесили на body
-тег.
Разберем остальные свойства. Как говорится во многих любовных романах — поймешь, когда потеряешь. Давайте так и поступим: посмотрим, что было бы, если бы я убрал все свойства, кроме mask-image
:
img {
mask-image: url(taiga-logo.svg);
}
Мы получили слишком много елочек, потому что на огромную картинку с закатом наложили крошечную картинку-маску. Исходные размеры svg-лого — лишь 68 × 60 пикселей. По умолчанию браузер пытается уместить как можно больше масок, если такое возможно. Чтобы конфигурировать такое поведение, нам и нужно свойство mask-repeat. Вернем его на место:
img {
mask-image: url(taiga-logo.svg);
mask-repeat: no-repeat;
}
Обновленная ситуация выглядит так:
Повторяющиеся елочки убрали, но вот размер явно не тот. Знакомимся со свойством mask-size, которое принимает два значения — ширину и высоту изображения-маски. В данном случае мы хотим, чтобы наше лого было в полный рост, а ширина изображения растянулась, сохранив изначальные пропорции.
Наше изображение близко к заветному, но пока прибито к левому краю. Вооружаемся свойством mask-position, которое может принимать до двух аргументов: сдвиг по горизонтальной и вертикальной оси. Аргумент может быть и один, тогда он применяется на обе оси сразу.
Поэкспериментировать с только что описанными примерами можно в StackBlitz-примере:
Мы разобрали минимальную базу, которую необходимо знать про CSS-маски. Если в процессе дальнейшего чтения возникнут трудности в интерпретации синтаксиса или основных концепций маскирования, то моя личная рекомендация – предварительно заглянуть в статью Ахмад Шадида «CSS masking». А теперь переходим к реальным кейсам!
Слишком много букв — проблема, с которой постоянно приходится сталкиваться в вебе. Даже начинающие специалисты прекрасно знают, как решить ее с помощью CSS-свойства text‑overflow. Достаточно передать в него elipsis
— и переполнение контента будет «отрублено» троеточием. Это база.
Но вот приходит дизайнер и говорит, что мы теперь в дизайн-системе не хотим видеть троеточий. Хотим плавное угасание переполненного контента — как на иллюстрации ниже:
Для блока, в котором предусмотрен лишь однострочный текст, проблема решается очень просто:
.fade {
mask-image: linear-gradient(to right, black 80%, transparent 90%);
}
Мы сообщили браузеру, что пусть в качестве маски выступает линейный градиент, который слева направо первые 80% окрашен в черный, а последние 10% — полностью прозрачный, а 10% между полностью черной зоной и полностью прозрачной зоной (промежуток 80—90%) пусть будет плавный переход с постепенным угасанием черного в прозрачный цвет.
Сейчас в DevTools не существует удобных инструментов для дебага CSS-масок. Но есть лайфхак: при разработке временно менять
mask-image
наbackground-image
. Это позволит визуально видеть, что из себя представляет маска: все непрозрачное остается, все прозрачное вырезается.
Решением этой проблемы я мало кого впечатлю, оно встречается почти в каждом учебном материале про CSS-маски. Но усложним задачу.
Приходит дизайнер с новой «радостной» новостью, что мы хотим отображать затухание текста не только для однострочного контента, но и для многострочного. Например, видны полностью только первые две строчки длинного текста, третья пусть постепенно затухает, а все последующие строки пусть будут и вовсе скрыты.
Прошлое решение тут уже не подойдет. А подойдет многослойная маска. У каждого элемента может быть не одна маска, а сразу несколько ее слоев. По синтаксису все просто: во все изученные свойства через запятую передаем характеристики каждого нового слоя:
.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-image
мы через запятую перечислили два слоя нашей маски.
Первый слой — это градиент, переходящий из черного в черный, то есть по факту сплошный черный на всем своем промежутке. Просто передать слово black
не сработает.
Второй слой — это почти тот же самый градиент, что мы использовали и для однострочного текста в прошлом решении, но поменяли местами цвета black
и transparent
. Через свойства mask-position
и mask-size
мы определяем, где и как будут располагаться слои маски: первый слой пусть занимает все пространство первых трех строк, а второй — лишь последнюю видимую третью строку.
Пока это все еще не будет работать, первый слой маски просто перекрывает все три первые строки, что делает второй слой полностью бесполезным. Нужно, чтобы содержимое второго слоя было исключено из первого слоя.
Познакомлю вас с еще одной возможной конфигурацией CSS-масок — mask-composite. Она принимает множество возможных значений. Если кратко, ее суть — конфигурировать, как будут комбинироваться слои масок между собой. По умолчанию (значение add
) они накладываются друг на друга, расширяя зону покрытия непрозрачной зоны маски.
В нашем случае это дефолтное поведение не подходит, а подходит свойство exclude, документация которого гласит: «Непересекающиеся регионы объединяются». Именно поэтому мы и сделали таким второй слой маски: его первые 80% прозрачны — а значит, не пересекаются с первым слоем. Остальная часть имеет полупрозрачный контент — именно он и пересечется с первым слоем и частично исключит эту зону из финального результата «коллаборации» двух слоев. Получаем финальное решение для переполнения контента у многострочного текста:
.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;
}
По ссылке можно посмотреть полученные финальные решения в качестве StackBlitz-примера:
А если вы хотите вдохновиться еще более сложным техническим решением (но зато еще более гибким), приглашаю вас взглянуть на тайговую директиву Fade.
Вам поступает новая задача — разработать инструмент, который будет визуально прятать любую часть контента от пользователя. Вот в таком виде:
В приложениях Т-Банка такая фича активно применяется, чтобы пользователь мог спрятать свои чувствительные данные — значение баланса или банковского счета, когда делает запись экрана или показывает что-то лично своим знакомым. Похожую фичу вы могли видеть в телеграм-каналах на тексте или картинке-спойлере.
Создадим картинку-маску. Нам нужна простая svg-шка, которая состоит из хаотичных квадратиков разной степени прозрачности:
<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>
Вариантов создания такой маски много. Можно наплодить через IDE таких тегов rect
, можно попросить дизайнера сотворить такое в Figma, а можно взять наше готовое решение. Главное, чтобы финальный вариант был похож на что-то подобное. Важно использовать степени прозрачности, а не оттенки серого!
Дальше хитрость в том, что ничего не мешает заинлайнить полученную svg-шку внутрь CSS-свойства:
.sensitive {
background: currentColor;
mask-image: url('data:image/svg+xml,<svg width="360" ...>...</svg>');
mask-size: auto 100%;
}
При использовании этого приема в разных местах браузер не будет грузить svg-шку повторно, он самостоятельно позаботится о кэшировании.
Мы намерено задаем свойство background: currentColor
— полупрозрачные квадратики маски примут оттенок цвета текста, который они закрывают.
И вновь успех! Приглашаю изучить исходники нашего тайгового компонента Sensitive и прикладываю StackBlitz-пример для экспериментов:
Пришло время научиться создавать кастомизируемые чек-боксы, не плодя лишнюю HTML-вложенность, а лишь прибегая к силам CSS-маски.
При многолетней разработке Taiga UI мы всегда старались не плодить лишнюю вложенность разметки без необходимости. Пренебрежение этим правилом усложняет кастомизируемость компонентов, что приходится компенсировать раздуванием количества публичных CSS-переменных.
Для начала возьмем нативный <input type="checkbox" />
и отключим у него всю встроенную браузерную кастомизацию через appearance: none
. После закастомизируем внешний вид «коробочки» от чек-бокса:
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;
}
Добавим поведение, что при выбранном состоянии чек-бокс плавно заливается цветом. Здесь нам пригодится псевдокласс :checked:
input[type='checkbox'] {
/* [...] Портянка уже ранее объявленных свойств */
background: transparent;
transition: background-color 0.3s;
}
input[type='checkbox']:checked {
background: lavender;
}
Осталось добавить галочку. Воспользуемся псевдоэлементом :after
и своими знаниями CSS-маскирования:
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
позволило нам спозиционировать псевдоэлемент :after
во всю ширину и высоту хоста. Потом заливаем весь псевдоэлемент в оттенок черного. А через mask-image
вырезаем форму галочки с помощью простой инлайн-иконки. Все остальные строки отвечают за плавную анимацию появления и исчезновения галочки.
Посмотрите получившийся результат в действии:
Тайговая реализацию компонента checkbox основывается на описанном решении. Но при этом таит в себе еще больше интересных приемов, которые уже выходят за рамки данной статьи. Обязательно загляните на досуге!
Надеюсь, у меня получилось убедить, что CSS-маскирование — очень интересный и полезный инструмент в арсенале любого фронтенд-разработчика. Глубокие его знания могут творить настоящие чудеса!
Приведенные в этой статье кейсы не единственные хитрости CSS-маскирования, которые вы сможете отыскать в нашей библиотеке компонентов Taiga UI. Но не хочется утомлять слишком длинным чтивом за раз. Да и мне нужна передышка, чтобы собраться с мыслями для объяснения более сложных примеров.
В продолжении разберем остальные наши примеры, базированные на CSS-маскировании: как и зачем мы наслаивали радиальные градиенты в ProgressSegmented, хитрости работы с новым компонентом Icon, а также рассмотрим непростой компонент Switch.
До скорых встреч!