Карточный домик стилизации без чистого CSS. Часть 1

от автора

Всем прекрасного времени суток. Это первая часть из серии двух статей про перенос стилизации с 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

Изначально структура наших модулей была следующей:

sass модули

sass модули

Я принялся переносить все переменные и нормализующие стили на 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'ам и их анимацией.

Буду рад вашей обратной связи.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Была ли полезна вам эта статья

50% Да1
50% Нет1

Проголосовали 2 пользователя. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/articles/863874/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *