Negative Border Radius – не больно
Код шагов для доклада «Negative Border Radius — не больно».
https://frontendconf.ru/moscow/2025/abstracts/15798
Попробуем «нарисовать» впуклый угол с помощью нескольких CSS-градиентов. Чтобы видеть результат, добавим градиенты как фоновые изображения.
Когда добьёмся нужной формы, заменим фон на маску.
Сейчас важно получить только правильную форму угла. Поэтому размеры градиентов задаём произвольно с помощью CSS-свойства background-size.
Временно используем полупрозрачный цвет, чтобы видеть границы отдельных «деталей» будущей формы.
С помощью четырёх линейных градиентов мы получили основную форму маски. Они заполняют весь контейнер, оставляя три «ступеньки» в правом нижнем углу.
Прямоугольные ступеньки скруглим с помощью радиальных градиентов.
С помощью радиальных градиентов нарисуем два круга и одну четверть круга.
Давайте разберёмся, как с помощью радиальных градиентов нарисовать круг и четверть круга с резкими краями.
Начнём с простейшего градиента с принудительной круглой формой и двумя цветами.
Чтобы получить резкие края круга, задаём смежным колорстопам одинаковые позиции цвета.
Нам нужен круг, края которого всегда примыкают к границам своего фрагмента фона, независимо от размеров фрагмента.
Казалось бы, если задать позицию цвета на 100%, резкий переход смещается к краям контейнера. Однако на практике первый цвет градиента заливает весь контейнер, и вместо круга мы получаем квадрат.
Чтобы получить нужный эффект, необходимо изменить размер радиального градиента. Добавим ключевое слово closest-side, которое делает радиус градиента равным расстоянию от центра до ближайшей стороны.
Делаем второй цвет прозрачным и получаем круг с резкими краями, который точно вписан в свой фрагмент фона.
Края круга всегда примыкают к границам фрагмента фона. Именно то, что нужно!
Теперь нарисуем четверть круга. Временно вернём второй цвет.
По умолчанию центр градиента располагается в центре контейнера.
Если сместить центр круга в правый нижний угол, то его размер начнёт уменьшаться. Всё дело в closest-side. Чем ближе центр круга к одной из сторон, тем меньше расстояние от центра до ближней к нему стороны, а значит, меньше и радиус.
Давайте поменяем размер градиента на farthest-side. В этом режиме радиус градиента становится равен расстоянию от его центра до дальней стороны. Фигура стала похожа на четверть круга.
Теперь мы можем поместить центр градиента в правый нижний угол. В результате получим настоящую четверть круга!
Для создания впуклого угла нам нужна внешняя часть четверти круга. Поэтому делаем прозрачным первый цвет градиента.
Возвращаемся к нашей заготовке. Первый круг мы уже поместили в верхнюю «ступеньку». Для этого пришлось подобрать не только размер, но и правильную позицию фона — 100% 100px.
Добавим второй круг в нижнюю «ступеньку».
И добавим четверть круга в среднюю «ступеньку». Форма впуклого угла готова.
Теперь поменяем цвет всех градиентов на сплошной чёрный, чтобы получить итоговую заготовку для маски.
Момент истины. Раскомментируем фоновую заливку в верхнем CSS-правиле. Заменим все свойства, начинающиеся с «background-» на свойства, начинающиеся с «mask-» во втором CSS-правиле. Маска сработала!
Семейства свойств background-* и mask-* ведут себя максимально единообразно. Можно только поблагодарить авторов спецификации за такой продуманный подход.
Добавим кастомизацию угла с помощью CSS-переменных. Закомментируем фон в первом CSS-правиле, а во втором обратно заменим mask-* на background-* и вернём полупрозрачные цвета.
Компонент будет принимать три параметра:
--nb-r — радиус скругления;--nb-w — дополнительная ширина впуклого угла;--nb-h — дополнительная высота впуклого угла.
В изначальной заготовке все эти размеры были равны 40px, поэтому зададим всем параметрам такое же значение.
Для каждого градиента заменим статичные значения на выражения с параметрами. После замены изображение не должно измениться.
Первый градиент выделен красным цветом. Подберём для него вертикальный размер фона, который задает высоту прямоугольника.
Полученное выражение:
высота контейнера минус высота угла минус три радиуса
По аналогии подберём высоту второго прямоугольника. Она меньше высоты первого на один радиус. Поэтому итоговое выражение выглядит следующим образом:
высота контейнера минус высота угла минус два радиуса
Высота третьего прямоугольника:
высота контейнера минус один радиус
Четвёртый прямоугольник занимает всю высоту контейнера, поэтому его не изменяем.
Благодаря calc(100% - …) мы смогли привязать вычисление высоты к полной высоте блока и получили универсальные выражения, которые зависят только от значений входных параметров.
Зададим ширину прямоугольников. Первый прямоугольник занимает всю ширину контейнера, поэтому его не изменяем.
Ширина второго:
ширина контейнера минус один радиус
Ширина третьего:
ширина контейнера минус ширина угла минус два радиуса
Ширина четвёртого прямоугольника:
ширина контейнера минус ширина угла минус три радиуса
У прямоугольников закончились статичные значения, которые можно заменить на выражения с параметрами.
Изменим входные параметры и проверим, что картинка из прямоугольников получается правильной.
Вернём круглые «детали» и начнём подбирать выражения для них.
Размер верхнего и нижнего кругов равен двум радиусам, а размер среднего круга равен одному радиусу.
Выражения для размеров скруглений готовы. Осталось подобрать выражения для их положения.
Верхний круг прижат к краю контейнера по горизонтали, поэтому подбираем выражение для его позиции по вертикали. Экспериментальным путём получилось:
высота контейнера минус высота угла минус два радиуса
Нижний круг прижат к краю контейнера по вертикали. Поэтому подбираем выражение для позиции по горизонтали:
ширина контейнера минус ширина угла минус два радиуса
Позиция среднего скругления по горизонтали:
ширина контейнера минус ширина угла минус один радиус
По вертикали:
высота контейнера минус высота угла минус один радиус
Снова изменим параметры и протестируем поведение фонов. Круглые части отображаются правильно.
Вернём полностью чёрный цвет фонов.
И снова заменим background-* на mask-*. Всё работает.
Вынесем стили масок в отдельный файл — nebo.css — и заключим их внутрь CSS-правила с классом .nebo, что расшифровывается как «negative border radius».
Подключим файл nebo.css перед основным стилевым файлом и добавим карточкам в разметке класс nebo. Также добавим дополнительные классы-модификаторы, которые будут отвечать за расположение впуклого угла.
Добавим в .nebo дополнительные CSS-переменные: --_nb-curve-pos и --_nb-smooth.
С помощью --_nb-curve-pos будем управлять направлением среднего выреза. С помощью --_nb-smooth можно настраивать уровень «сглаживания» круглых частей маски.
Заменим статичное расположение центра 100% 100% на переменную --_nb-curve-pos в последнем радиальном градиенте.
Вы можете поэкспериментировать со значением --_nb-curve-pos, изменяя его на 0 0, 100% 0 и 0 100%. Вы увидите, как «поворачивается» среднее скругление.
Внедрим переменную --_nb-smooth вместо статичных значений позиции цвета в радиальных градиентах.
Модификатор .nebo--tl помещает впуклый угол в верхний левый угол контейнера.
Внутри CSS-правила .nebo--tl переопределяем значение переменной --_nb-curve-pos на 0 0. Также переопределяем значение CSS-свойства mask-position.
Выражения для расположения градиентов подбираются таким же способом, как и ранее.
Модификатор .nebo--tr помещает впуклый угол в верхний правый угол контейнера.
Внутри правила «поворачиваем» средний угол и переопределяем позиции градиентов.
Добавляем правило .nebo--bl для размещения впуклого угла в нижнем левом углу.
Для симметрии добавляем пустое правило .nebo--br, так как стили внутри .nebo рассчитаны на правый нижний угол.
Поэкспериментируем с параметрами различных блоков. У второго блока уменьшим радиус, зададим нулевую высоту и увеличим ширину впуклого угла. Получилась фигура, похожая на папку или на браузер с вкладкой.
Чем меньше радиус впуклого угла, тем меньше можно делать значение --_nb-smooth. Для больших радиусов значение --_nb-smooth можно устанавливать в районе 99.5%.
Переопределим параметры третьего блока.
Параметры впуклых углов можно даже анимировать, хотя библиотека изначально на это не была рассчитана.
Во время анимации в области среднего скругления могут мерцать белые полоски.
Чтобы избавиться от эффекта мерцания, можно на полпикселя увеличить радиус градиента, который рисует четвертинку круга.
Размер четвертинки увеличится, она начнёт немного «заползать» на соседние градиенты маски, и эффект мерцания исчезнет.
Перед вами адаптивная карточка на контейнерных запросах с впуклым углом в правом нижнем углу.
Можно ли сделать так, чтобы впуклый угол перемещался в другое место при срабатывании контейнерного запроса? Например, в левый верхний угол карточки.
Для этого выполним небольшой рефакторинг библиотеки. Поместим сложные составные значения масочных свойств в промежуточные CSS-переменные.
Сначала добавим четыре «библиотечные» переменные, которые описывают направление среднего скругления.
Затем зададим переменной --_nb-curve-pos значение из библиотеки:
--_nb-curve-pos: var(--_nb-curve-pos-br);
Затем удалим значение свойства mask-image и запишем его в промежуточную CSS-переменную --_nb-mask-image.
Ту же операцию выполним для CSS-свойства mask-size.
Зададим значения CSS-свойств mask-image и mask-size из промежуточных переменных --_nb-mask-image и --_nb-mask-size.
Продолжаем рефакторинг.
Удалим значение свойства mask-position во всех CSS-правилах. Затем внутри CSS-правила .nebo добавим ещё один набор «библиотечных» переменных для позиции фона.
Добавим промежуточную переменную --_nb-mask-position и запишем в неё одно из библиотечных значений:
--_nb-mask-position: var(--_nb-mask-position-br);
Внутри .nebo зададим для CSS-свойства mask-position значение из промежуточной переменой --_nb-mask-position.
Внутри классов-модификаторов переопределим значения промежуточных переменных на подходящие библиотечные значения. Например, для верхнего левого угла используем библиотечные переменные с суффиксом -tl:
--_nb-curve-pos: var(--_nb-curve-pos-tl);
--_nb-mask-position: var(--_nb-mask-position-tl);
По аналогии переопределим значения промежуточных переменных в других модификаторах.
Мы получили точно такое же поведение, как и до рефакторинга. Может показаться, что рефакторинг не дал нам ничего нового и просто усложнил код. Но это не так.
Подключим обновлённую версию nebo.css к нашей карточке.
Зададим ширину контейнера 600px, чтобы контейнерный запрос сработал и отобразилась «широкая» версия карточки.
Теперь переопределим значения промежуточных переменных внутри контейнерного запроса. То есть мы сделали то же самое, что и внутри классов-модификаторов.
Благодаря рефакторингу мы можем менять расположение впуклого угла с помощью всего двух строк кода, без изменения разметки.
Параллельно мы можем менять и основные входные параметры библиотеки.
Код библиотеки доступен на гитхабе — https://github.com/htmlacademy/nebo.css.