[Перевод] О конфликтах Sass и сравнительно новых возможностей CSS
Сравнительно недавно в CSS появилось много интересных возможностей, таких, как CSS-переменные и новые функции. Хотя всё это и может сильно упростить жизнь веб-дизайнерам, эти возможности способны неожиданными способами взаимодействовать с CSS-препроцессорами вроде Sass.
Автор материала, перевод которого мы сегодня публикуем, расскажет о том, с какими проблемами ей довелось столкнуться, о том, как она с ними справлялась, и о том, почему она считает, что без Sass в наши дни всё ещё обойтись нельзя.
Ошибки
Если вы экспериментировали с CSS-функциями min()
и max()
, то, используя разные единицы измерения, могли столкнуться с сообщениями об ошибках, наподобие такой: Incompatible units: vh and em
.
Сообщение об ошибке, возникающее при использовании различных единиц измерения в функциях min () и max ()
Это сообщение выводится из-за того, что в Sass есть собственная функция min()
. CSS-функция min()
, в результате, игнорируется. Кроме того, Sass не может выполнять вычисления, используя единицы измерения, между которыми нет чётко зафиксированной взаимосвязи.
Например, взаимосвязь единиц измерения cm
и in
определена чётко, поэтому Sass может найти результат функции min(20in, 50cm)
и не выдаёт ошибку в том случае, если чем-то подобным воспользоваться в коде.
То же самое происходит и с другими единицами измерения. Например, все угловые единицы измерения взаимосвязаны: 1turn
, 1rad
или 1grad
всегда приводятся к одним и тем же значениям, выраженным в единицах измерения deg
. То же самое справедливо, например, и в случае, когда 1s
всегда равно 1000ms
. 1kHz
всегда равно 1000Hz
, 1dppx
всегда равно 96dpi
, 1in
всегда равно 96px
. Именно поэтому Sass может преобразовывать друг в друга значения, выраженные в этих единицах измерения, и смешивать их в вычислениях, используемых в различных функциях, вроде собственной функции min()
.
Но всё идёт не так, когда между единицами измерения нет чёткой взаимосвязи (как, например, выше, у em
и vh
).
И такое происходит не только при использовании значений, выраженных в разных единицах измерения. Попытка использования функции calc()
внутри min()
тоже приводит к появлению ошибки. Если попробовать, в min()
, поместить что-то вроде calc(20em + 7px)
, то выводится такая ошибка: calc(20em + 7px) is not a number for min
.
Сообщение об ошибке, возникающее при попытке использования calc () внутри min ()
Ещё одна проблема появляется в ситуации, когда пытаются использовать CSS-переменную или результат работы математических CSS-функций (вроде calc()
, min()
, max()
) в CSS-фильтрах наподобие invert()
.
Вот сообщение об ошибке, которую можно увидеть в подобных обстоятельствах: $color: 'var(--p, 0.85) is not a color for invert
Использование var () в filter: invert () приводит к ошибке
То же самое происходит и с grayscale()
: $color: ‘calc(.2 + var(--d, .3))‘ is not a color for grayscale
.
Использование calc () в filter: grayscale () приводит к ошибке
Конструкция filter: opacity()
тоже подвержена подобным проблемам: $color: ‘var(--p, 0.8)‘ is not a color for opacity
.
Использование var () в filter: opacity () приводит к ошибке
Но другие функции, используемые в filter
, включая sepia()
, blur()
, drop-shadow()
, brightness()
, contrast()
и hue-rotate()
, работают с CSS-переменными совершенно нормально!
Оказалось, что причина этой проблемы похожа на ту, которой подвержены функции min()
и max()
. В Sass нет встроенных функций sepia()
, blur()
, drop-shadow()
, brightness()
, contrast()
, hue-rotate()
. Но там есть собственные функции grayscale (), invert () и opacity (). Первым аргументом этих функций является значение $color
. Ошибка появляется из-за того, что при использовании проблемных конструкций такого аргумента Sass не находит.
По той же причине проблемы возникают и при использовании CSS-переменных, представляющих как минимум два hsl()
или hsla()
-значения.
Ошибка при использовании var () в color: hsl ()
С другой стороны, без использования Sass конструкция color: hsl(9, var(--sl, 95%, 65%))
— это совершенно правильная и совершенно нормально работающая CSS-конструкция.
То же самое справедливо и для таких функций, как rgb()
и rgba()
:
Ошибка при использовании var () в color: rgba ()
Кроме того, если импортировать Compass и попытаться использовать CSS-переменную внутри linear-gradient()
или radial-gradient()
, можно столкнуться с ещё одной ошибкой. Но, в то же время, в conic-gradient()
переменными можно пользоваться без всяких проблем (конечно, если браузер эту функцию поддерживает).
Ошибка при использовании var () в background: linear-gradient ()
Причина проблемы кроется в том, что в Compass есть собственные функции linear-gradient () и radial-gradient()
, а вот функции conic-gradient()
там никогда не было.
В целом же, все эти проблемы произрастают из того факта, что в Sass или в Compass есть собственные функции, имена которых совпадают с теми, что есть и в CSS. И Sass, и Compass, встречая эти функции, считают, что мы собираемся пользоваться именно их собственными реализациями этих функций, а не стандартными.
Вот засада!
Решение проблемы
Эту проблему можно решить, если вспомнить о том, что Sass чувствителен к регистру, а CSS — нет.
Это означает, что можно написать что-то вроде Min(20em, 50vh)
и Sass не распознает в этой конструкции свою собственную функцию min()
. Никаких ошибок при этом выдано не будет. Эта конструкция будет представлять собой правильно сформированный CSS-код, который работает именно так, как ожидается. Аналогично, избавиться от проблем с другими функциями можно, нестандартным образом записывая их имена: HSL()
, HSLA()
, RGB()
, RGBA()
, Invert()
.
Если говорить о градиентах, то тут я обычно использую такую форму: linear-Gradient()
и radial-Gradient()
. Делаю я это из-за того, что такая запись близка к именам, используемым в SVG, но в данной ситуации сработает и любое другое подобное имя, включающее в себя хотя бы одну заглавную букву.
Зачем все эти сложности?
Почти каждый раз, когда я пишу в Твиттере что-нибудь про Sass, мне начинают читать лекции о том, что сейчас, когда есть CSS-переменные, Sass пользоваться уже не нужно. Я решила, что мне стоит на это ответить и объяснить причину моего несогласия с этой идеей.
Во-первых — отмечу, что я считаю CSS-переменные чрезвычайно полезными, и то, что я пользовалась ими для решения множества задач последние три года. Но я полагаю, что необходимо помнить о том, что их использование сказывается на производительности. А поиск проблемы, возникшей в лабиринте вызовов calc()
, может оказаться пренеприятнейшим занятием. Стандартные браузерные инструменты разработчика пока не очень хороши в этом деле. Я стараюсь не увлекаться использованием CSS переменных, чтобы не попадать в ситуации, в которых их минусы показывают себя сильнее, чем их плюсы.
Не так-то легко понять то, какими будут результаты вычисления этих выражений calc ()
В целом, если переменная используется как константа, не изменяется от элемента к элементу, или от состояния к состоянию (а в подобных случаях CSS-переменные, определённо, использовать нужно), или если переменная не уменьшает объёма скомпилированного CSS-кода (решая проблему повторений, создаваемую префиксами), тогда я буду использовать Sass-переменную.
Во-вторых — поддержка переменных всегда была довольно-таки незначительной причиной среди тех причин, по которым я использую Sass. Когда я начала пользоваться Sass, а было это во второй половине 2012 года, я сделала это, в основном, ради циклов. Ради той возможности, которой до сих пор нет в CSS. Хотя я перенесла некоторую логику, связанную с циклами, в препроцессор HTML (так как это уменьшает объём сгенерированного кода и позволяет избежать необходимости модифицировать и HTML, и CSS), я всё ещё использую циклы Sass во множестве случаев. Среди них — генерирование списков значений, создание значений для настройки градиентов, создание списков точек при работе с функцией polygon()
, создание списков трансформаций и так далее.
Ниже показан пример того, как я поступила бы раньше при создании некоторого количества HTML-элементов с помощью препроцессора. То, какой именно это препроцессор, особой роли не играет, но я выбрала Pug:
- let n = 12;
while n--
.item
Затем я бы создала переменную $n
в Sass (и в этой переменной должно было бы быть то же значение, что и в HTML) и запустила бы с её использованием цикл, в котором сгенерировала бы трансформации, используемые для позиционирования каждого из элементов:
$n: 12;
$ba: 360deg/$n;
$d: 2em;
.item {
position: absolute;
top: 50%; left: 50%;
margin: -.5*$d;
width: $d; height: $d;
/* аккуратно оформим стили */
@for $i from 0 to $n {
&:nth-child(#{$i + 1}) {
transform: rotate($i*$ba) translate(2*$d) rotate(-$i*$ba);
&::before { content: '#{$i}' }
}
}
}
Минус этого всего заключается в том, что мне пришлось бы менять значения и в Pug-коде, и в Sass-коде в том случае, если изменилось бы количество элементов. В коде, кроме того, появляется много повторений.
CSS-код, сгенерированный на основе вышеприведённого кода
Теперь я перешла к другому подходу. А именно, с помощью Pug я генерирую индексы в виде пользовательских свойств, а затем использую их при объявлении transform
.
Вот код, который планируется обработать с помощью Pug:
- let n = 12;
body(style=`--n: ${n}`)
- for(let i = 0; i < n; i++)
.item(style=`--i: ${i}`)
Вот Sass-код:
$d: 2em;
.item {
position: absolute;
top: 50%;
left: 50%;
margin: -.5*$d;
width: $d;
height: $d;
/* аккуратно оформим стили */
--az: calc(var(--i)*1turn/var(--n));
transform: rotate(var(--az)) translate(2*$d) rotate(calc(-1*var(--az)));
counter-reset: i var(--i);
&::before { content: counter(i) }
}
Здесь можно поэкспериментировать с этим кодом.
Элементы, сгенерированные и стилизованные с использованием циклов
Применение такого подхода значительно уменьшило объём CSS-кода, сгенерированного автоматически.
CSS-код, сгенерированный на основе вышеприведённого кода
Но если нужно создать что-то вроде радуги, без Sass-циклов не обойтись.
@function get-rainbow($n: 12, $sat: 90%, $lum: 65%) {
$unit: 360/$n;
$s-list: ();
@for $i from 0 through $n {
$s-list: $s-list, hsl($i*$unit, $sat, $lum)
}
@return $s-list
}
html { background: linear-gradient(90deg, get-rainbow()) }
Вот рабочий вариант этого примера.
Многоцветный фон
Конечно, это можно сгенерировать и средствами переменных Pug, но у такого подхода нет преимуществ перед динамической природой CSS-переменных, и он не позволит уменьшить объём кода, передаваемого браузеру. В результате мне нет смысла отказываться от того, к чему я привыкла.
Я много использую встроенные математические функции Sass (и Compass), такие, как тригонометрические функции. В наши дни подобные функции являются частью спецификации CSS, но их поддержка реализована пока не во всех браузерах. В Sass таких функций нет, но в Compass они есть, и именно поэтому мне часто приходится применять Compass.
И, конечно, я могу написать в Sass собственные функции такого рода. Я так делала в самом начале, до того, как в Compass появилась поддержка обратных тригонометрических функций. Мне эти функции очень нужны, поэтому я написала их сама, пользуясь рядами Тейлора. Но в наши дни эти функции есть в Compass. Они лучше и производительнее тех, которые я писала сама.
Математические функции очень важны для меня по той причине, что я — программист, а не художник. Значения в моём CSS-коде обычно формируются на основе математических вычислений. Это — не какие-то «магические числа», или что-то такое, что играет чисто эстетическую роль. В качестве примера их использования можно привести генерирование списка правильных или квазиправильных многоугольников для clip-path
. Подобное применяется, например, при создании чего-то вроде аватаров или стикеров, форма которых отличается от прямоугольной.
Рассмотрим правильный многоугольник, вершины которого лежат на окружности. Перетаскивание слайдера в следующем примере, с которым можно поэкспериментировать здесь, позволяет нам увидеть то, где размещаются точки для фигур с разным количеством вершин.
Фигура с тремя вершинами
Вот как выглядит соответствующий Sass-код:
@mixin reg-poly($n: 3) {
$ba: 360deg/$n; // базовый угол
$p: (); // список координат точек, изначально пустой
@for $i from 0 to $n {
$ca: $i*$ba; // текущий угол
$x: 50%*(1 + cos($ca)); // координата x текущей точки
$y: 50%*(1 + sin($ca)); // координата y текущей точки
$p: $p, $x $y // добавление координат текущей точки к списку координат
}
clip-path: polygon($p) // установка списка точек в качестве значения clip-path
}
Обратите внимание на то, что мы тут применяем циклы и другие конструкции, пользоваться которыми, применяя чистый CSS, очень неудобно.
Немного более продвинутая версия этого примера может включать в себя вращение многоугольника, реализованное путём добавления одного и того же смещения ($oa
) к углу, соответствующему каждой вершине. Это можно видеть в следующем примере. Здесь генерируются звёзды, которые устроены похожим образом, но всегда имеют чётное количество вершин. При этом каждая вершина с нечётным индексом располагается на окружности, радиус которой меньше основной окружности ($f*50%
).
Звезда
Можно наделать и таких интересных звёздочек.
Звёзды
Можно создавать стикеры с границами (border
), созданными с использованием необычных шаблонов. В данном примере стикер создаётся из единственного HTML-элемента, а шаблон, используемый для настройки border
, создаётся с применением clip-path
, циклов и математических вычислений в Sass. На самом деле, вычислений тут довольно много.
Стикеры со сложными границами
Ещё один пример представлен созданием фона для карточек. Здесь, в цикле, с помощью оператора получения остатка от деления и экспоненциальных функций, создаётся фон с имитацией эффекта дизеринга.
Эффект дизеринга
Здесь тоже интенсивно используются CSS-переменные
Далее, можно вспомнить об использовании миксинов ради избежания необходимости снова и снова писать одинаковые объявления при стилизации чего-то вроде слайдеров. Разные браузеры используют различные псевдоэлементы для стилизации внутренних компонентов подобных элементов управления, поэтому нужно, для каждого компонента, задавать стили, которые управляют их видом с использованием разных псевдоэлементов.
К сожалению, в CSS, каким бы заманчивым это ни выглядело, нельзя поместить что-то вроде следующего кода:
input::-webkit-slider-runnable-track,
input::-moz-range-track,
input::-ms-track { /* общие стили */ }
Работать это не будет. Весь этот набор правил игнорируется в том случае, если хотя бы один селектор не будет распознан. И, так как ни один браузер не знает о существовании всех трёх селекторов из этого примера, эти стили не будут применены ни в одном браузере.
Если нужно, чтобы стилизация, всё же, заработала, надо будет поступить примерно так:
input::-webkit-slider-runnable-track { /* общие стили */ }
input::-moz-range-track { /* общие стили */ }
input::-ms-track { /* общие стили */ }
Но это может привести к тому, что одни и те же стили появятся в коде три раза. А если нужно, скажем, поменять у track
свойство background
, это будет значить, что придётся редактировать стили в ::-webkit-slider-runnable-track
, в ::-moz-range-track
и в ::-ms-track
.
Единственное вменяемое решение этой задачи заключается в использовании миксинов. Стили повторяются в скомпилированном коде, так как без этого никак не обойтись, но теперь нам, по крайней мере, не приходится три раза вводить в редакторе один и тот же код.
@mixin track() { /* общие стили */ }
input {
&::-webkit-slider-runnable-track { @include track }
&::-moz-range-track { @include track }
&::-ms-track { @include track }
}
Итоги
Главный вывод, который я могу сделать, получился таким: Sass в — это то, без чего нам пока не обойтись.
Пользуетесь ли вы Sass?