Как мы подружили SCSS с CSS Variables на примере c темизацией UI Kit


    Всем привет, меня зовут Виталик, я senior фронтенд-разработчик Skyeng. Наша команда делает онлайн-платформу Vimbox для изучения английского языка. Примерно год назад мы с дизайнером доделали небольшой UI kit, искоренивший хаос в интерфейсе и кодовой базе.


    Оказалось, что в компании не мы одни хотели UI kit, и к нам стали приходить другие команды за советом «как написать собственный». Нам удалось отговорить их от этой затеи, пообещав темизировать свой – это сэкономило компании сотни часов разработки. Выбирая решение, мы рассмотрели Angular Material, кастомизированные сборки и CSS Variables и в итоге остановились на последних, несмотря на их слабую совместимость с SCSS, основой имевшегося UI kit. Под катом – подробности того, что мы сделали.



    Проблема


    Первый UI kit состоял из шрифтов, палитры, набора элементов для создания форм (инпут, кнопка и тд), системы управления svg иконками. Ещё был реализован popup и tooltip на основе Angular materials. Он был заточен под работу только с «классическим» Vimbox: многие вещи были осознанно намертво зашиты и не допускали изменений извне. А у Skyeng начали появляться новые продукты на той же платформе, например, для детей.


    Разработчики новых направлений, зная, что у нас что-то есть, пришли за советами. Причем, к нашему удивлению, приходили уже с макетами своих UI kit'ов: они собирались разрабатывать свои решения с нуля, т.к. им нужен был другой внешний вид компонентов. Было ясно, что что-то идет не так, и мы предложили доработать нашу библиотеку, добавив возможность ее темизации.


    Аргументация была простая: на проектирование нашего UI kit ушло 200 часов UX дизайна и более 500 часов разработки. Это время, необходимое для создания системы шрифтов, цветов и около 10 базовых компонентов. Соответственно, если писать для каждого продукта отдельную библиотеку, компания потратит N*500 часов времени разработчиков. Мы считали, что совершенствование нашего UI kit обойдется дешевле, плюс это действие не придется повторять для каждого продукта.


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


    Исходные данные


    Наши инструменты: Angular, SCSS.


    Поддерживаем мы только современные браузеры и, с некоторыми ограничениями, IE11. Что здорово упрощает жизнь.


    Все наши UI kit компоненты были пронизаны общими стилями, которые мы складывали в UI kit.var.scss в качестве SCSS констант:


    @mixin fontSizeXl {
    @include fontSize(18px, 26px);
    }
    
    $colorSkillListening: #9679e0;
    $colorSkillListeningText: #7754d1;
    $colorSkillListeningBackground: mix($colorSkillListening, #ffffff, 16%);
    $colorSkillListeningBackgroundHover: mix($colorSkillListening, #ffffff, 8%);

    Задача


    • Все новые продукты собираются из уже имеющихся элементов, присутствующих во «взрослом» Vimbox – комнаты для занятий, личные кабинеты и т.д.
    • Дизайнеры должны иметь широкую свободу реализации творческих замыслов, отличительных особенностей и специфических требований новых продуктов.
    • При этом сохраняется преемственность, т.е. сколь бы кислотные цвета и безумные шрифты ни выдумал дизайнер, принадлежность результата его работы к экосистеме Skyeng остается очевидной.
    • Все это добавляется к уже имеющемуся UI kit, сохраняя все его преимущества.

    Поехали!


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


    Angular Material


    Мы не любим писать велосипеды, поэтому в первую очередь обратились к Angular Material. В компонентах динамические стили вынесены в отдельный {component}-theme.scss файл. Эти стили привязываются к глобальному селектору компонента.


    плюсы минусы
    не нужно добавлять новые технологии в проект нужно вносить изменения в каждый компонент
    самый большой объем работ
    придётся пожертвовать инкапсуляцией стилей

    CSS Variables


    У нас отличный повод попробовать модный CSS Variables. План – пересадить кастомизируемые части UI kit на CSS Variables. В компонентах используются те же SCSS-константы, но вместо конкретных значений в них прописаны CSS vars.


    плюсы минусы
    обладает скоупом придется добавить полифилл для IE
    нативная технология браузеров (за исключением IE) мы не сможем использовать SCSS функции
    открывает возможность применения CSS vars в других задачах

    Кастомизированные сборки


    Мы любим простые решения, почему бы не попробовать изменить сборку? Каждая команда создает свой файлик с настройкой темы. При сборке для всех кастомных тем создаются отдельные бандлы со своей темой.


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

    Решение


    Неделю мы изучали каждый вариант, обсуждали, откладывали решение и снова изучали.


    Мы любим новые технологии и следим за ними, но внедряем только в том случае, если они дают нам реальные бонусы. Мы знали о CSS Variables, нам хотелось их попробовать, но отсутствие функций SCSS вызывало большую печаль. И все же плюсы этого варианта были очевидны, мы решили разобраться, как и какие функции SCSS мы используем, можно ли подружить их с CSS vars.


    Разбираемся с проблемами CSS vs SCSS


    Поэкспериментировав, мы поняли, что основная проблема состоит в отсутствии поддержки #hex в CSS: в SCSS мы пишем rgba(#ffffff, 0.4), а в CSS то же самое требует другого набора параметров — rgba(255, 255, 255, 0.4). У нас все работает с #hex, и мы очень, очень не хотим это менять. Мы нашли решения, расскажу в порядке поступления.


    Lighten & Darken


    Наш дизайнер придумал палитру, состоящую из небольшого количества базовых цветов, расширяющуюся за счет SCSS-функций lighten и darken:


    // $color – базовый цвет
    base: $color,
    background: mix($color, #ffffff, 16%),
    backgroundHover: mix($color, #ffffff, 8%),
    hover: lighten($color, 5),
    focused: darken($color, 5),
    ...more transformations...

    Мы попробовали найти аналог lighten и darken в CSS, но ничего не обнаружили. Думали несколько дней, пока не осознали, что для кастомизации нам нужно избавиться от этих функций внутри библиотеки, вынеся их наружу. Ведь каждая команда может захотеть придумать свою собственную формулу изменения цвета при изменении фокуса – например, коллегам из Kids нужно больше контрастности.


    Получилось простое решение – перенести наши преобразования на сторону платформы, которая будет инициализировать тему. И вот для платформы мы пишем функцию, которая автоматически создает нужные значения:


    @function getMainColors($color, $colorText) {
    $colors: (
     text: $colorText,
     base: $color,
     background: mix($color, #ffffff, 16%),
     backgroundHover: mix($color, #ffffff, 8%),
     lightenLess: lighten($color, 5),
     darkenLess: darken($color, 5),
     lightenMore: lighten($color, 20),
    );
    
    @return $colors;
    }

    Платформа использует ее при инициализации цветов:


    //platform
    $colorValues: (
    brand: getMainColors(#5d9cec, #4287df),
    positive: getMainColors(#8cc152, #55a900),
    accent: getMainColors(#ff3d6f, #ff255d),
    wrong: getMainColors(#ff6666, #fe4f44),
    )

    RGBA


    В своем UI kit мы используем функцию rgba. С ее помощью мы регулируем прозрачность базовых цветов. Но если в SCSS rgba работает с форматом #hex, то CSS этого не может. Пришлось написать функцию, которая раскладывает #hex значение в r/g/b:


    // returns `r, g, b` from `#hex` for `rgba(var(--smth))` usage
    @function rgbValuesFromHex($hex) {
    @return red($hex), green($hex), blue($hex);
    }

    Ну и поскольку мы не хотим ко всей палитре генерировать RGB значения ручками, создаем отдельную функцию, делающую это рекурсивно для каждого цвета в коллекции:


    // adds `fieldRgb: r, g, b` fields to map for each `field: #hex` for `rgba(var(--smth-rgb))` usage
    @function withRgbValues($map) {
    $rgbValues: ();
    
    @each $name, $value in $map {
     $formattedValue: ();
    
     @if type-of($value) == 'map' {
     $rgbValues: map-merge($rgbValues, (#{$name}: withRgbValues($value)));
     } @else {
    //добавляем рядом с цветом, rgb версию с постфиксом Rgb в имени
     $rgbValues: map-merge($rgbValues, (#{$name}Rgb: rgbValuesFromHex($value)));
     }
    }
    
    @return map-merge($map, $rgbValues);
    }

    В итоге вот так выглядит инициализация палитры со сгенерированными значениями RGB:


    $colorValues: withRgbValues(
    (
     text: (
     base: #242d34,
     secondary: #50575c,
     label: #73797d,
     placeholder: #969b9e,
     inversed: #ffffff,
     inversedSecondary: #dadada,
     ),
     brand: getMainColors(#5d9cec, #4287df),
     positive: getMainColors(#8cc152, #55a900),
     accent: getMainColors(#ff3d6f, #ff255d),
     wrong: getMainColors(#ff6666, #fe4f44),
    //...etc

    На выходе получаем карту цветов SCSS, которую затем можно отдать в метод, превращающий ее в CSS variables. Для того, чтобы достать из темы RGB значение, написали функцию:


    @function getUiKitRgbVar($path...) {
    $path: set-nth($path, -1, #{nth($path, -1)}Rgb); //постфикс который мы добавили в функции выше
    @return getFromMap($uiKitBaseVars, $path...);
    }
    
    //пример вызова
    border-color: rgba(getUiKitRgbVar(color, brand, base), $opacity64);

    Превращаем SCSS const в CSS vars


    Первый шаг – завести зеркальную структуру (аналогичную SCSS), в которой хранятся имена CSS Variables:


    $colorCssVars: withRgbCssVars(
    (
     text: (
     base: getColorCssVar(text, base),
     secondary: getColorCssVar(text, secondary),
     label: getColorCssVar(text, label),
     placeholder: getColorCssVar(text, placeholder),
     inversed: getColorCssVar(text, inversed),
    //и так далее

    getColorCssVar – метод, добавляющий префиксы названиям переменных. Добавляем префикс --sky, чтобы избежать коллизии с внешними библиотеками. А также добавляем к --sky префикс библиотеки - UI kit, чтобы избежать коллизий с внутренними библиотеками. Получился --sky- UI kit:


    @function getColorCssVar($parts...) {
    @return getUiKitCssVar(color, $parts...);
    }
    
    @function getUiKitCssVar($parts...) {
    $uiKitCssVarPrefix: '--sky- UI kit';
    
    $cssVar: $uiKitCssVarPrefix;
    
    @each $part in $parts {
     $cssVar: $cssVar + '-' + $part;
    }
    
    @return $cssVar;
    }

    Например, для getColorCssVar(text, base) на выходе получим --sky- UI kit-color-text-base.


    Финальный штрих – рекурсивный миксин, инициализирующий значения из структуры SCSS в переменные с названиями из структуры CSS Var:


    //вешаем миксин на главный тег нашего приложения
    :root {
    @include uiKitThemeCssVars($uiKitDefaultTheme); //uiKitDefaultTheme – SCSS структура библиотеки с дефолтными значениями темы
    }
    
    @mixin uiKitThemeCssVars($theme) {
    $cssVarsList: createVarsList($theme, $uiKitBaseCssVars); //18+, страшный метод превращающий Map в List, $uiKitBaseCssVars структура имён css переменных
    
    @each $cssVar, $value in $cssVarsList {
     #{$cssVar}: $value;
    }
    }

    Пример использования темы на платформе:


    .popup {
    font-family: getUiKitVar(font, family);
    background-color: getUiKitVar(color, background, base);
    ...
    }

    Что в итоге


    Мы смогли использовать CSS Variables, cохранив возможность использования SCSS функций. Создали возможность кастомизации внешности компонентов. Написали пару рекурсивных методов для автоматизации расширения темы. Ну и главное – потратили 30 часов разработки вместо N*500.


    Profit!

    • +17
    • 4,7k
    • 9
    Skyeng
    228,38
    Компания
    Поделиться публикацией

    Комментарии 9

      0
      Не понял зачем вы используете CSS variables, если вы не хотите отказываться от SCSS. Только потому что CSS variables — новое и «модное»?
        0
        Я вот тоже не понял. Если в рантайме цвета не менять, а у них супорт ie11, зачем нужны CSS Custom Properties?
          0

          Почти уверен, что это связано со скоупами. Например, есть отдельная страница, если переобъявить css-переменные в ней, то их подхватят все компоненты за раз. С SCSS нужно будет делать отдельный файл для этой страницы/части страницы.

            0
            Полифил для IE работает только Ahead-of-Time, это те же sass-переменные которые выглядят как CSS Custom Properties.
          +2
          Различия зависят от реализации scss темизации.

          Мы рассматривали подход из Angular Materials. В material каждому компоненту заводится миксин с темизирующей частью, затем все миксины объединяются в один с помощью объекта темы и инициализируются на корне приложения. Нам не понравились следующие вещи:
          — необходимо жертвовать инкапсуляцией стилей
          — нужно вносить изменения в каждый компонент, это создаёт много работы для реализации темизации а так же дополнительные работы при создании новых компонентов
          — каждая тема доступная на странице раздувает файл стилей одинаковыми селекторами

          Подход с хранением всех тем в ui-kit нас не устроил потому что много продуктов, соответственно сильно раздуется библиотека.

          Может есть ещё какие-то подходы которые мы упустили?
            0
            codyhouse.co/blog/post/css-custom-properties-vs-sass-variables
            В этой статье хорошо описывают разницу и мотивацию выбора css переменных
            0
            Мне всегда казалось что для цветовых палит логичнее использовать цвета в hsla. А ещё лутчше в перцепционной цветовые модели (lab, luv и т.д.)

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

          Самое читаемое