Всем прекрасного времени суток. Это первая часть из серии двух статей про перенос стилизации с SCSS'а на чистый CSS.
Сегодня мы с вами посмотрим каким образом можно преобразовать миксины SCSS'а на CSS с атомарными классами. Как я уже писал в прошлой статье, я работаю в достаточно молодой компании уровня стартапа, поэтому мы сами открываем методы оптимизации и некоторые особенности CSS'а.
Итак начнём.
С чего всё началось
Нам было необходимо создать функционал в SCSS, который позволит сделать по-настоящему "резиновый" шрифт – при изменении размера экрана динамически меняется размер текста: font-size, line-height. Мой коллега нашёл неплохой способ реализовать это на SCSS через миксин.
@mixin adaptiv-font-engine($maxSize, $minSize, $lineHeightDelta, $maxWidth, $minWidth) { $fontCoef: $maxSize - $minSize; $widthCoef: $maxWidth - $minWidth; $size: calc(#{$minSize}px + #{$fontCoef} * ((100vw - #{$minWidth}px) / #{$widthCoef})); font-size: $size; line-height: calc(#{$size} + #{$lineHeightDelta}px); }
Дописав этот "движковый" миксин, я сделал центральный миксин, который достаточно долго и использовался в наших проектах.
Неудобный и большой миксин для адаптирования шрифта из "движка":
Полный код миксина с медийными запросами
@mixin adaptiv-font($desktopFont, $laptopFont, $tabletFont, $mobileFont) { @include adaptiv-font-engine( list.nth($desktopFont, 1), list.nth($laptopFont, 1), list.nth($desktopFont, 2) - list.nth($desktopFont, 1), $desktop-size-max, $desktop-size-min ); @include laptop-media() { @include adaptiv-font-engine( list.nth($laptopFont, 1), list.nth($tabletFont, 1), list.nth($laptopFont, 2) - list.nth($laptopFont, 1), $laptop-size-max, $laptop-size-min ); } @include tablet-media() { @include adaptiv-font-engine( list.nth($tabletFont, 1), list.nth($mobileFont, 1), list.nth($tabletFont, 2) - list.nth($tabletFont, 1), $tablet-size-max, $tablet-size-min ); } @include mobile-media() { @include adaptiv-font-engine( list.nth($mobileFont, 1), list.nth($mobileFont, 1), list.nth($mobileFont, 2) - list.nth($mobileFont, 1), $mobile-size-max, $mobile-size-min ); } }
Центральный миксин для работы со шрифтами:
/* Mixin can get 1, 2, 3 and 4 arguments of tuple of font-size and line-height 1 - all 2 - other, mobile 3 - other, tablet, mobile 4 - desctop, laptop, tablet, mobile */ @mixin font-size($args...) { @if (list.length($args) == 1) { @include adaptiv-font(list.nth($args, 1), list.nth($args, 1), list.nth($args, 1), list.nth($args, 1)); } @else if (list.length($args) == 2) { @include adaptiv-font(list.nth($args, 1), list.nth($args, 1), list.nth($args, 1), list.nth($args, 2)); } @else if (list.length($args) == 3) { @include adaptiv-font(list.nth($args, 1), list.nth($args, 1), list.nth($args, 2), list.nth($args, 3)); } @else if (list.length($args) == 4) { @include adaptiv-font(list.nth($args, 1), list.nth($args, 2), list.nth($args, 3), list.nth($args, 4)); } }
Пример работы с этим миксином:
@mixin M-Size() { @include font-size((20, 26), (18, 22), (16, 20)); } @mixin M-Medium() { font-family: "Roboto-Medium"; @include M-Size(); } @mixin M-Regular() { font-family: "Roboto-Regular"; @include M-Size(); }
Первые проблемы
Слишком страшный и сложно поддерживаемый код
Такое можно понять в силу вшитой адаптивности и логики в "резиновости" самого шрифта
Также приходилось в конфигурации проекта глобально импортировать файл, который собирает вышеописанные миксины в одном месте:
export default defineConfig({ plugins: [vue(), vueDevTools()], css: { preprocessorOptions: { scss: { additionalData: ` @import "@/assets/styles/global.scss"; `, }, }, }, });Это мне крайне не нравилось с точки зрения оптимизации
CSS-перформанса
К этому добавлялись обновления самого CSS'а, за которыми SCSS'у приходилось следовать. Я говорю о нативном нестинге в CSS, который начали серьёзно обсуждать ещё с 128-го Chrome'а (примерно). Само собой такую технологию в будущем эту технологию поддержали и Safary и Mazilla.
И однажды прийдя на работу и обновив локальный модуль SASS'а я получил огромное полотно варнингов от него. Постоянно была жалоба на так называемый Legacy JS API. Мы смогли избавиться от них добавив в vite.config.js поле api: "modern-compiler" .
export default defineConfig({ plugins: [vue(), vueDevTools()], css: { preprocessorOptions: { scss: { additionalData: ` @import "@/assets/styles/global.scss"; `, api: "modern-compiler", }, }, }, });
Но беда не приходит одна и оказалось, что теперь нельзя использовать миксины посередине стилей, наподобе:
.some-class { width: 100px; height: 100px; @include M-Medium(); color: current-color; }
Это рушило логику нативного нестинга уже CSS'а. Для эмитации похожего поведения приходилось ухитряться и писать следующим образом:
.some-class { width: 100px; height: 100px; @include M-Medium(); & { color: current-color; } }
Что было ещё хуже, чем ранее написанные миксины. Было принято решение использовать подобные решения в крайне редких и нетривиальных ситуациях.
Но ...
Через неделю SASS (Dart) выкатил новое обновление, в котором предупреждалось о скором отключении полных импортов из сторонних модулей - разрешается использовать только @use.
Меня в край задолбали такие быстрые и серьёзные изменения, а серверную консоль хочется видеть без варнингов, и пришлось серьёзно взяться за работу.
Переход на CSS
Изначально структура наших модулей была следующей:

Я принялся переносить все переменные и нормализующие стили на CSS. Но когда столкнулся с миксинами и в особенности с миксином "резинового" размера текста впал в ступор.
Спустя некоторое время я наткнулся на интересную концепцию виртуальных переменных в CSS. Смысл следующий – мы создаём переменную в рамках определённого атомарного класса, которая принимает значение из другой переменной, а также имеет значение по умолчанию на случай отсутствия переменной-аргумента. Для обозначения данной переменной как виртуальной и ограниченной в своём классе мы делаем очень простой модификатор, который используется уже не один год в языках без инкапсуляции классов – добавляем нижнее подчёркивание.
Пример:
.atom-class { --_color: var(--color, #FFF); color: var(--_color); }
<span class="my-text atom-class">Hallo, world!!!</span>
Таким образом мы можем контролировать поведение атомарного класса из родного класса тега.
Пример:
.my-text { --color: #F00; }
Таким образом, мы можем создавать аналог миксинов на SCSS используя чистый CSS.
Продолжение идеи
Теперь вернёмся к нашим бараном, из-за которых весь сырбор.
Для более короткого решения приведу пример работы атамарного класса с адаптивным текстом (пока без резиновости):
[class*="static-font"] { --_font-size: var(--font-size, 1em); --_line-height: var(--line-height, calc(var(--_font-size) + 4px)); font-size: var(--_font-size); line-height: var(--_line-height); } .static-font__M { --font-size: 20px; --line-height: 26px; @media (max-width: 1024px) and (min-width: 510px) { --font-size: 18px; --line-height: 22px; } @media (max-width: 509px) { --font-size: 16px; --line-height: 20px; } }
Note: Необходимость в таком нестандартном селекторе
[class*="static-font"]обоснована тем, что атомарный класс может располагаться в любом месте аттрибутаclass. Если же использовать[class^="static-font"], то это будет работать исключительно в случаях когда сам аттрибут начинается на описанную строку –class="static-font__M my-text".
Описанным способом можно создавать атомарные классы для множества стандартных размеров в рамках вашего дизайн-кода.
Для интересующихся оставлю ниже полностью переписанный на CSS вариант резинового текста:
Движок резинового текста
[class*="responsive-font"] { /* Require props */ --_max-font-size: var(--max-font-size); --_max-line-height: var(--max-line-height); --_max-screen-width: var(--max-screen-width); --_min-font-size: var(--min-font-size); --_min-line-height: var(--min-line-height); --_min-screen-width: var(--min-screen-width); /* ============= */ /* Computed deltas */ --font-delta: (var(--_max-font-size) - var(--_min-font-size)); --line-height-delta: (var(--_max-line-height) - var(--_min-line-height)); --screen-width-delta: (var(--_max-screen-width) - var(--_min-screen-width)); /* =============== */ --main-coef: (100vw - var(--_min-screen-width) * 1px) / var(--screen-width-delta); /* Target values */ --computed-font-size: calc(var(--_min-font-size) * 1px + var(--font-delta) * var(--main-coef)); --computed-line-height: calc(var(--_min-line-height) + var(--line-height-delta) * var(--main-coef)); /* ============= */ font-size: clamp(calc(var(--_min-font-size) * 1px), var(--computed-font-size), calc(var(--_max-font-size) * 1px)); line-height: clamp(calc(var(--_min-line-height) * 1px), var(--computed-line-height), calc(var(--_max-font-size) * 1px)); }
Реализация конкретного размера текста
.responsive-font__M { --max-screen-width: var(--desktop-size-max); --max-font-size: 20; --max-line-height: 26; --min-screen-width: var(--tablet-size-max); --min-font-size: 18; --min-line-height: 22; @media (max-width: 1024px) and (min-width: 510px) { --max-screen-width: var(--tablet-size-max); --max-font-size: 18; --max-line-height: 22; --min-screen-width: var(--mobile-size-max); --min-font-size: 16; --min-line-height: 20; } @media (max-width: 509px) { --max-screen-width: var(--mobile-size-max); --max-font-size: 16; --max-line-height: 20; --min-screen-width: var(--mobile-size-min); --min-font-size: 16; --min-line-height: 20; } }
Переменные размеров экрана
:root { /* ===== Desktop ===== */ --desktop-size-max: 1920; --desktop-size-min: 1441; /* =================== */ /* ===== Laptop ===== */ --laptop-size-max: 1440; --laptop-size-min: 1025; /* ================== */ /* ===== Tablet ===== */ --tablet-size-max: 1024; --tablet-size-min: 510; /* ================== */ /* ===== Mobile ===== */ --mobile-size-max: 509; --mobile-size-min: 350; /* ================== */ }
К сожалению, логика резинового текста не позволила сделать реализацию короче и легче к прочтению, но данная реализация является менее нагруженной с точки зрения сборки проекта, перформанса самого CSS'а, а также является более удобной в использовании новому члену команды.
Развитие идеи
Мне так понравилась идея виртуальных переменных в CSS, что я решил не останавливаться на достигнутом и создал несколько похожих атомарных классов-миксинов:
.clamp-text-lines { --_lines-count: var(--lines-count, 3); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--_lines-count); overflow: hidden; }
.custom-scrollbar { --_scrollbar-color: var(--scrollbar-color, #3E3E3E); --_scrollbar-width: var(--scrollbar-width, 4px); scrollbar-gutter: auto; } .custom-scrollbar::-webkit-scrollbar { width: var(--_scrollbar-width); height: var(--_scrollbar-width); } .custom-scrollbar::-webkit-scrollbar-thumb { border-radius: 100px; background-color: var(--_scrollbar-color); } .custom-scrollbar::-webkit-scrollbar-track { background-color: #0000; border-radius: 100px; }
Заключение
Всем советую использовать классы подобного формата, но опять же с умом. Такой подход даёт возможность находу подправить нужный стиль, если не подходит выставленный по умолчанию, такие классы очень легко применять новым сотрудникам (им достаточно один раз увидеть как применяется класс).
А так же гибкость такого подхода подкрепляется нативным (низким) уровнем взаимодействия JS'а с CSS'ом, при необходимости изменить стили находу или по условию - больше никаких классов модификаторов (это, конечно же, тоже в меру).
Анонс
Думаю на следующей неделе выпустить вторую часть данной серии по нативным popover'ам и их анимацией.
Буду рад вашей обратной связи.
