Всем прекрасного времени суток. Это первая часть из серии двух статей про перенос стилизации с 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'ам
и их анимацией.
Буду рад вашей обратной связи.
Комментарии (15)
ImagineTables
04.12.2024 14:42сделать по-настоящему "резиновый" шрифт – при изменении размера экрана динамически меняется размер текста
И такой подход себя оправдывает?
Я предпочитаю каждый блок смотреть в процессе относительно всех брейкоинтов, подгоняя где размером, где геометрией.
aleks_andr_19 Автор
04.12.2024 14:42Соглашусь, дизайнерам не всегда получается вписаться в рамки 10 шрифтов. Но в случае когда надо в конкретном месте подменить некоторые показатели шрифта, это можно легко сделать CSS переменными прямо по селектору тега, на который навесили
static-font__M
В статье есть похожий пример с цветом текста:
.my-text { --color: #F00; }
dom1n1k
04.12.2024 14:42Непонятно, зачем все эти сложности с
line-height
и прибавляемой дельтой вместо простого безразмерного множителя.aleks_andr_19 Автор
04.12.2024 14:42Соглашусь, зачастую дизайнеры выставляют достаточно строго разницу между
font-size
иline-height
. Но случаются случаи когда необходимо увеличить или наоборот уменьшитьline-height
:Буквально недавно мы разговаривали с нашим лидом дизайна по поводу этого шрифта для кнопок – им было необходимо, чтобы
font-size
сохранился, ноline-height
был26px
.Относительно применения дельты, которую вы предложили – я пытался применить этот подход, но на практике он оказался неудобным, потому что в Figma
line-height
указан в конкретном значении и для указания значения дельты нужно делать доп услилие высчитывая её. В данном вопросе я придерживаюсь jobs to be done, чтобы сторонний разработчик просто скопировал значения из Figma и всё работало.dom1n1k
04.12.2024 14:42зачастую дизайнеры выставляют достаточно строго разницу между
font-size
иline-height
Это какой-то домысел, никто разницу не делает.
В реальности есть два подхода:
1. Высота строки привязывается к кеглю множителем, напримерline-height: 1.5;
2. Высота строки фиксируется в конкретном значении, напримерline-height: 24px;
– в частности, этот вариант полезен, когда высота строки должна быть кратна какой-то сетке, например 8-пиксельной. Но это не фиксированная дельта.aleks_andr_19 Автор
04.12.2024 14:42Под строгим выставлением дельты я имею ввиду, что в фигме явно выставлен и `font-size` и `line-height` – как раз второй вариант, про который вы говорили
dom1n1k
04.12.2024 14:42Ну хорошо, но в коде ведь дельта зачем-то прибавляется.
aleks_andr_19 Автор
04.12.2024 14:42SASS’овский миксин «резинового» текста был сделан на скорую руку и я не проводил тщательного ревью (я тимлид), поэтому решили оставить так
delphinpro
C ходу не могу понять необходимости селектора атрибута вместо обычного селектора класса. Почему не
.static-font { }
?aleks_andr_19 Автор
Ниже описан селектор
static-font__M
, который уже внутри себя имеет реализованные переменные для виртуальных переменных "миксина"delphinpro
Вы не ответили на вопрос. Что мешает использовать простой селектор класса для описания "миксина"?
aleks_andr_19 Автор
Иначе придётся реализовывать в каждом классе типо
static-font__XL
,static-font__L
,static-font__M
,static-font__S
и тд. :В кратце - для более лаконичного использования атомарного класса
delphinpro
не придется
https://codepen.io/delphinpro/pen/PwYPgVJ
aleks_andr_19 Автор
Да, соглашусь. Если вставить два атомарных класса, то это будет работать. Но мне нужно было реализовать это как миксин - то есть через одно имя. Должно работать так:
delphinpro
Теперь понятно.