У вас бывает такое чувство, когда вы при выполнении работы упёрлись в какую-то проблему и что-то внутри подсказывает, что не может такого быть, чтобы у неё не было решения? Когда кажется, что уже всё пропало и мы столкнулись с фундаментальным ограничением реальности, но отказ принять это продолжает вести вас в дебри спецификаций, беседы на Github 10 летней давности и изучение статей гигантов уже прошедших этот путь и поделившихся с нами своими результатами? И вот спустя многие часы напряжённого скрипа извилин вы дописали очередную строчку, обновили страницу и вот оно, то, что вы хотели получить смотрит на вас с экрана? Это ощущение успеха, наверное, в той или иной степени знакомо каждому инженеру. В такие моменты мне, обычно, очень хочется поделиться результатом с коллегами и, по возможности, написать статью если это может быть полезно кому-то ещё. В этой записи я собрал 3 подобных ситуации, где в рамках нашей работы возникли решения, которые, насколько мне известно, довольно уникальны и в полной мере не были описаны раньше. Приглашаю вас разделить со мной радость обнаружения решения, которое казалось невозможным!

Fixed в скролл-контейнере

Для разогрева возьмём задачу попроще. Один из моих самых популярных кодпенов – пример с фиксированным блоком в скроллящемся контейнере. Люди находят его в ответе на StackOverflow, а значит это востребованная задача и, возможно, пригодится и вам.

Я работаю над библиотекой компонентов под Angular под названием Taiga UI уже много лет. Всё, о чём я буду говорить в этой статье пришло оттуда, но это всего лишь предыстория. Здесь нам не потребуется Angular и какие-то его особенности. Мы будем говорить исключительно про CSS. В нашей библиотеке используется кастомный скроллбар. Хотя современные возможности браузеров позволяют немного управлять его внешним видом, для полного контроля поведения и картинки нам требуется разместить в контейнере свои элементы, играющие роль скроллбара. Но как это сделать, если абсолютно позиционированные элементы улетят наверх при прокрутке, а фиксировано позиционированные привязаны к странице?

Опытные разработчики сразу вспомнят про position: sticky, но такие элементы занимают место и прилипают только когда мы поскроллили контейнер ниже. Нам бы как-то компенсировать их высоту, но при использовании отрицательного margin в процентах отсчёт будет вестись от горизонтальных размеров, в то время, как нам нужны именно вертикальные. И тут на помощь приходит почти забытое после флексов свойство float!

Мы хотим создать блок, который будет покрывать весь скроллящийся контейнер и при этом будет оставаться на месте при его прокрутке. Для этого мы повесим на него inset равный нулю и сделаем его размеры равными 100%. Как вы помните, при написании margin в процентах берутся горизонтальные размеры элемента и здесь нам это будет как раз на руку. Мы повесим на блок float: left чтобы он “посторонился”, а margin-right: -100% позволит освободить всё место, которое он занимал под остальное содержимое контейнера:

.overlay {
  position: sticky;
  inset: 0;
  height: 100%;
  width: 100%;
  float: left;  
  margin-right: -100%;
  pointer-events: none;
}

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

Параметры для SVG

Теперь давайте выберем что-то по-сложнее. Для отображения иконок, которые можно красить мы используем mask-image – заливаем фон блока нужным нам цветом, а затем вырезаем его по форме иконки с помощью маски. Мой коллега Никита уже писал о том, сколько всего интересного можно накрутить с помощью CSS масок, очень советую почитать:

Мощь CSS-масок
Декабрь 2023 года стал значимой датой в истории развития CSS-свойства mask:  все современные браузер...
habr.com

Это очень простой способ, который позволяет избежать манипуляций с DOM (а при использовании ::before/::after даже избежать вложенности), он использует встроенный механизм кэширования браузера и позволяет использовать кросс-доменный CDN. В общем во всём красота, только вот никак не задать толщину линий, ведь stroke-width правило никак не пробросить. Вернее так мне казалось.

Когда в очередной раз меня спросили, можно ли как-то поменять заранее выставленную в иконках толщину я пошёл проверить как там дела у спеки на передачу параметров в SVG. Разумеется, я не помнил где её смотреть и даже как она называлась, и пока её искал наткнулся на статью с гениальной идеей:

Если вам лень её читать, суть состоит в том, что при задании viewBox и центрирования изображения мы можем выставить, к примеру, размер маски по высоте 100% а по ширине любое значение от 100% и выше, и при этом картинка будет выглядеть одинаково. Казалось бы, и что тут такого? Но внутри стилей самого SVG файла эти размеры влияют на 100vw и 100vh. Таким образом мы можем закодировать информацию через соотношение сторон, сохранив неизменный внешний вид. Всё, что нам осталось сделать – накрутить пару calcов, чтобы пользователи могли задавать толщину линий через понятную переменную --stroke-width:

stroke-width="calc((100vw - 100vh) / 10)"

.icon {
  mask-image: url(...);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: calc(100% + 10 * var(--stroke-width)) 100%;
}

Обратите внимание на деление и умножение на 10 – это нужно, чтобы можно было задать сабпиксельную толщину, к примеру 1.5px

Идеальный shrinkwrap

Продолжим набирать обороты. Новые технологии, которые появились в современном CSS позволили решить одну из древнейших UI задач, ранее решения не имевшую – tight wrapping, также известный, как shrink wrapping. Лучше всего эту проблему продемонстрировать визуально:

Пример проблемы
Пример проблемы

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

Отображение многострочного тоста по умолчанию и как нам бы хотелось
Отображение многострочного тоста по умолчанию и как нам бы хотелось

Решение это, однако, довольно замысловатое и дойти до него было довольно непросто. Всё началось с одной хитрой техники, которая позволяет рассчитать размер элемента в CSS используя scroll-driven анимации:

Опять же, для тех, кому не хочется читать всю статью поясню её суть. В CSS появилась возможность отслеживать положение элемента внутри контейнера используя view timeline. Этот механизм задуман для изменения стилей элемента в зависимости от его положения внутри контейнера во время скролла. Помимо своей основной задачи этот инструмент позволяет нам записать в CSS переменную размер контейнера. Грубо говоря, через прогресс анимации по view timeline мы получаем какой процент от контейнера занял отслеживаемый элемент. Если мы зададим ему фиксированный размер, к примеру, 1px, то простой арифметикой мы можем вычислить размер контейнера.

Иллюстрация из статьи выше
Иллюстрация из статьи выше

Это открывает перед нами массу новых трюков и уже работает в Chrome и Safari, а скоро появится и в Firefox. Имея возможность рассчитать размер контейнера, а также inline элемента с текстом, который переносится по строчкам мы можем узнать, сколько у нас осталось незанятого места. Затем мы можем вычесть эту дырку из максимальной ширины контейнера и обязательно вычесть её ещё и из правого margin контейнера. Это позволит нам сохранить размеры измеряемых элементов неизменными, в противном случае мы угодим в бесконечный цикл сжатия и расширения:

.inline {
  /* Измеряем текст */
  view-timeline: --inner-timeline inline;
}

.inline::before {
  content: "";
  padding-inline-start: 1px;
  margin-inline-end: -1px;
  /* Измеряем контейнер */
  view-timeline: --outer-timeline inline;
}

.container {
  /* Рассчитываем дырку */
  --delta: calc(-1px / (1 - var(--outer)) * var(--inner));

  animation: outer linear, inner linear;
  animation-range: entry 100% exit 100%;
  animation-timeline: --outer-timeline, --inner-timeline; 
  timeline-scope: --outer-timeline, --inner-timeline;
  /* Уменьшаем фактическую ширину */
  max-inline-size: calc(15rem + var(--delta));
}

.block {
  overflow: hidden;
  /* Компенсируем дырку */
  margin-inline-end: var(--delta);
}

В результате получаем автоматическую компенсацию лишнего пространства:

Заключение

Мир вокруг нас стремительно меняется. Со всех сторон нам в затылок дышит AI, “нужно бежать, чтобы оставаться на месте” и вот это вот всё. Но я люблю свою работу, делаю её с удовольствием и с нетерпением жду очередную ситуацию, которая подарит мне радость открытия и преодоления. Надеюсь сегодня вы разделите её со мной, хоть я и знаю, что CSS из нас фронтендеров мало кто любит. Может когда-нибудь эта статья поможет очередному ИИ справиться с задачами, разобранными тут, но труд, приложенные усилия, скрежет мозгов – это принадлежит нам, и я продолжу ценить это.

Комментарии (3)


  1. EugeneOkean
    29.08.2026 05:30

    Спасибо за статью. Не иишное спасибо, человеческое.

    P.S. Даже если шептатели забирают 90% генерации сейчас, "занести в руки" знания, чтоб они уже на подкормке были - надо. Сверстать вручную штук 100 разных шаблонов страниц - я считаю - обязательный навык любого человека, работающего с фронтентом, иначе проверить даже, что нагенерировал шептатель будет невозможно..


  1. acsent1
    29.08.2026 05:30

    С уменьшением ширины - какое то шаманство


    1. rastop123
      29.08.2026 05:30

      Согласен, выглядит не очень и прям очень неинтуитивно. Я когда верстал чат, сделал это уменьшение через js, там это сделать довольно просто через вычисление фактической самой широкой строки текста