Пару лет назад в CSS произошла тихая революция, вызвавшая тектонический сдвиг в разработке интерфейсов. Вкратце — нам разрешили «сверлить» настоящие дырки в блоках.

Создание блоков с вырезами всегда было трудоёмким, даже для вырезов простейшей формы. Фронтендеры годами тренировались мысленно рассекать блоки на части: прямоугольничек для контента, прямоугольничек для выреза и ещё парочка рядом с ним. И вдруг парадигма поменялась...

Простые вырезы теперь делаются в десять раз быстрее. Одной строчкой кода. Да, надо менять мышление и забывать про нарезку блоков. И как же это приятно!

В статье мы сверстаем карточку с круглым вырезом двумя способами: традиционно‑дедовским и современным. Затем сравним объём кода, простоту и гибкость получившихся реализаций. И порадуемся, что будущее уже наступило!

Типовые требования к поведению компонента

Мы будем верстать эту карточку:

Дизайнерская карточка про реку Устью Архангельской области
Дизайнерская карточка про реку Устью Архангельской области

При реализации блоков с такими вырезами обычно учитывают два требования:

  • Под карточкой находится неоднородный фон: фотография или градиент, который должен по‑настоящему «просвечивать» в вырезе.

  • Карточка может изменять размеры. Если карточка резиновая, то она может изменять ширину, а при изменении контента может изменяться высота.

Парадигма приклеивания к блокам

У опытного верстальщика при взгляде на такой вырез возникают флэшбеки, а затем одна и та же схема:

Схема нарезки блока для создания выреза
Схема нарезки блока для создания выреза

Мы ищем прямоугольную область, в которой будет «жить» контент (выделена жёлтым). А затем «приклеиваем» к ней дополнительные элементы, которые формируют вырез.

Обычно вырез раскладывается на три части:

  • центральную — с фиксированными размерами (выделена зелёным);

  • верхнюю и нижнюю — которые растягиваются по высоте карточки (выделены красным).

Абсолютно типичный и рабочий подход. Давайте его реализуем.

Классическая реализация

Шаг 0. Базовая разметка

Начнём с самой простой разметки: одна карточка и контент внутри.

<div class="card">
  <p class="tag">Устьянский район</p>
  <h1 class="headline">Осенние берега Устьи</h1>
  <p class="lead">Здесь природа говорит шёпотом —<br> ощути северную тишину.</p>
  <button class="button">Купить билет</button>
</div>

Пока это просто набор текстовых элементов.

Разметка контентных элементов. Без жёлтого фона даже лучше, но дизайнер хочет и фон, и вырез
Разметка контентных элементов. Без жёлтого фона даже лучше, но дизайнер хочет и фон, и вырез

Шаг 1. Планируем "нарезку" компонента

Классическую реализацию мы сделаем с помощью флексбокса, чтобы она была понятна всем. Да, можно использовать и гриды, и позиционирование, но сути это не изменит. Поэтому используем флексбокс и планируем дополнительные обёртки с учётом этого.

Чтобы получить карточку с вырезом, нам понадобится две колонки. В одной колонке будет «жить» вырез, в другой — контент. Внутри колонки с вырезом будет три блока: верхний, центральный и нижний.

Центральный блок будет иметь фиксированную высоту, а крайние — растягиваться, подстраиваясь под высоту карточки.

Шаг 2. Добавляем обёртки в разметку

Добавим дополнительные обёртки.

<div class="card">
  <div class="cutout">
    <div class="cutout-top"></div>
    <div class="cutout-center"></div>
    <div class="cutout-bottom"></div>
  </div>
  <div class="card-content">
    <p class="tag">Устьянский район</p>
    <h1 class="headline">Осенние берега Устьи</h1>
    <p class="lead">Здесь природа говорит шёпотом —<br> ощути северную тишину.</p>
    <button class="button">Купить билет</button>
  </div>
</div>

Уже сейчас видно, как DOM начал разрастаться.

Шаг 3. Базовая геометрия карточки

Делаем карточку флекс‑контейнером и задаём ширины колонок. Нам приходится подбирать ширину блоков и внутренние отступы, чтобы получить нужное расстояние от контента до выреза.

.card {
  display: flex;
  width: 620px;
}

.card-content {
  width: 420px;
  padding: 50px;
  padding-left: 75px;
  color: #6b5742;
  background-color: #fdf6f0;
}

.cutout {
  width: 75px;
}

Теперь у нас есть две колонки: контент и будущий вырез.

Геометрия колонок. Колонка для выреза временно обведена красным
Геометрия колонок. Колонка для выреза временно обведена красным

Шаг 4. Стилизуем блоки вокруг выреза

Превращаем блок .cutout во флекс‑контейнер и направляем его главную ось вниз. Верхний и нижний блоки растягиваем по высоте с помощью flex-grow: 1; и задаём им фон. Центральному блоку, внутри которого будет изображение для выреза, задаём фиксированную высоту.

.cutout {
  display: flex;
  flex-direction: column;
}

.cutout-top,
.cutout-bottom {
  flex-grow: 1;
  background-color: #fdf6f0;
}

.cutout-center {
  height: 150px;
}

При изменении высоты карточки крайние элементы растягиваются, центральный остаётся фиксированным.

Тянущиеся блоки вокруг блока с вырезом
Тянущиеся блоки вокруг блока с вырезом

Шаг 5. Рисуем сам вырез

Форму выреза можно реализовать по‑разному: использовать PNG с полупрозрачностью, или SVG, или нарисовать с помощью CSS‑градиента. Мы используем радиальный градиент, чтобы не подключать лишнюю картинку.

.cutout-center {
  height: 150px;
  background-image: radial-gradient(
    circle at 0 50%,
    transparent 74.5px,
    #fdf6f0 75px
  );
}

Вырез появился. Карточка выглядит как задумано.

Вырез готов
Вырез готов

Проверяем на переполнение

Добавим больше текста. Высота карточки увеличилась — вырез остался на месте. Всё работает.

Поведение выреза при увеличении высоты карточки
Поведение выреза при увеличении высоты карточки

Реализация готова. Можно подвести итог. Нам понадобилось 5 дополнительных обёрток и около 20 строчек кода. Это не смертельно. Карточка ведёт себя хорошо. За исключением одного неприятного ограничения.

Ограничение — неоднородный фон у карточки

Попробуем добавить карточке градиентный фон. Добавляем его блоку с контентом.

.card-content {
  background-image: linear-gradient(135deg, #fff6e9, #fdd9b5, #f4a261);
}

Нам нужно, чтобы фон заполнял всю карточку, поэтому добавим его и блокам .cutout-top и .cutout-bottom:

.cutout-top,
.cutout-bottom {
  background-image: linear-gradient(135deg, #fff6e9, #fdd9b5, #f4a261);
}

И тут становится больно. Карточка состоит из отдельных блоков и фон ломается на стыках.

Проблема с неоднородным фоном
Проблема с неоднородным фоном

Состыковать фон разных частей принципиально невозможно, так как размеры карточки могут меняться.

Выводы по классическому подходу

Классический способ работает, но:

  • требует дополнительных обёрток;

  • заставляет писать много CSS;

  • не позволяет задавать неоднородный фон самой карточке.

А теперь давайте посмотрим, как ту же задачу можно решить иначе.

Современная реализация

Ключевая идея проста: вместо того чтобы склеивать карточку из отдельных кусочков, мы делаем вырез в нужном месте карточки.

Планируем место, где будет вырез на цельной карточке
Планируем место, где будет вырез на цельной карточке

Для этого нам понадобятся CSS‑маски.

Шаг 1. Возвращаемся к простой карточке

Убираем все дополнительные обёртки и оставляем один элемент .card. Единственное изменение — размер отступа слева. Увеличиваем его с учётом размера выреза.

.card {
  width: 420px;
  padding: 50px;
  padding-left: 150px;
  color: #6b5742;
  background-color: #fdf6f0;
}

Результат такой:

Базовая геометрия карточки
Базовая геометрия карточки

Шаг 2. Добавляем одно свойство — mask-image

.card {
  mask-image: radial-gradient(
    circle at 0 50%,
    transparent 74.5px,
    black 75px
  );
}

Всё. Карточка с вырезом готова.

Вырез на CSS-маске готов
Вырез на CSS-маске готов

Лирическое отступление: как работает эта маска

Не так много разработчиков знакомы с CSS‑масками, да и радиальные градиенты никогда не отличались популярностью. Поэтому давайте подробнее разберём, как сделан этот вырез.

Семейство свойств mask-* работает почти так же как семейство свойств background-*. Если вы работали с фоновыми изображениями, то и с масками разберётесь быстро.

Чтобы понять механику, заменим mask-image на background-image. Это позволит увидеть форму маски. Вначале создадим простейший радиальный градиент, круглой формы с двумя цветами (прозрачным и чёрным):

background-image: radial-gradient(
  circle,
  transparent,
  black
);

Результат:

Простейший радиальный градиент в качестве фона
Простейший радиальный градиент в качестве фона

Теперь добавим резкий цветовой переход между цветами, задав очень близкие позиции цвета в колорстопах. Разница в полпикселя нужна, чтобы сгладить края перехода и убрать эффект зубцов.

background-image: radial-gradient(
  circle,
  transparent 74.5px,
  black 75px
);

Результат:

Градиент с резким переходом цвето��
Градиент с резким переходом цветов

Сместим центр градиента на середину левой стороны блока с помощью at 0 50%. Форма маски готова.

background-image: radial-gradient(
  circle at 0 50%,
  transparent 74.5px,
  black 75px
);

Результат:

Центр градиента на середине левой стороны карточки. Заготовка для маски завершена
Центр градиента на середине левой стороны карточки. Заготовка для маски завершена

Прозрачная часть — это то, что будет вырезано. Чёрная — то, что останется. Чтобы получить вырез, остаётся только заменить background-image на mask-image.

Переполнение и неоднородный фон

Благодаря использованию маски карточка остаётся цельной. Поэтому:

  • она корректно реагирует на изменение высоты и ширины;

  • карточке можно задавать неоднородный фон (градиент, изображение);

  • можно комбинировать несколько фонов.

.card {
  background-image: 
    linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 50%, transparent 51%), 
    linear-gradient(135deg, #fff6e9, #fdd9b5, #f4a261);
  background-repeat: repeat-y, no-repeat;
  background-size: 1.5px 20px, auto auto;
  background-position: 115px 4px, 0 0;
}

В результате мы получили карточку с неоднородным фоном и «линией отреза»:

Никаких проблем с неоднородным фоном
Никаких проблем с неоднородным фоном

Выводы по современной реализации

Реализация выреза на CSS‑масках на порядок лучше старого подхода:

  • дополнительные обёртки не нужны;

  • используется всего одно CSS‑свойство;

  • позволяет задавать неоднородный фон самой карточке.

Поддержка CSS-масок — всё отлично!

Маски доступны во всех современных браузерах уже несколько лет, а радиальные градиенты — и того дольше.

Смена парадигмы

Старая парадигма «приклеивания к блокам», в которой мы собирали вырезы из нескольких частей, умирает. На замену ей приходит парадигма «вырезания из блоков», которая на порядок упрощает создание вырезов и даёт дополнительную гибкость при использовании фонов.

Будущее уже здесь!

А где взять ещё больше сложного CSS и код всех примеров?

Подписывайтесь на мой телеграм‑канал «CSS Боль». Там собраны все материалы, видеоролики, ссылки на интерактивные пошаговые демки