Именование переменных в CSS

Источник: «Naming Variables In CSS»
"Именовать вещи сложно", — гласит аксиома программной инженерии, и CSS не является исключением. Здесь собраны некоторые соображения, связанные с именованием пользовательских свойств CSS. Я буду использовать термины "переменная" и "пользовательское свойство" как взаимозаменяемые, поскольку для целей определения того, как их называть, это фактически одно и то же.

Оглавление

Отказ от ответственности: всё, что написано ниже, не является Евангелием. Для меня CSS — очень поэтичный язык, в нем так много разных способов выразить одни и те же понятия. Мне нравятся эти соглашения, но я не считаю их единственно верным способом именования переменных в CSS. Если вы не согласны с каким-либо из моих пунктов, буду рад узнать вашу точку зрения.

Соглашение об именовании

Первое о чём нужно поговорить в именовании переменных, это о том, какой стиль именования использовать. Похоже, индустрия остановилась на "kebab-case" (что вполне логично), но я думаю, стоит рассмотреть альтернативу.

Возможно, camelCase не так уж плох

Возможно, вы удивитесь, узнав, что многие собственные значения, определённые в CSS, не используют kebab-case. Например, currentColor и все именованные цвета (cadetBlue, rebeccaPurple, antiqueWhite и т. д.).

Рассмотрим возможность смешивания kebab-case с camelCase

Мы можем использовать camelCase в сочетании с kebab-case для создания структурно согласованных имён переменных. Идея заключается в том, чтобы использовать дефисы для отделения типа значения и пространства имён от имени переменной, а затем camelCase внутри каждого сегмента. По сути: namespaceName-valueType-variableName. Назовём это нотации триптиха. На мой взгляд, эта конвенция позволяет с первого взгляда понять, что является реальным именем переменной, а что — метаданными, закодированными в этом имени.

:root {
/* Сложнее просматривать: */
--system-control-accent-color: blue;
--system-focus-ring-color: cadetBlue;
--system-label-color-quaternary: lightGray;
--system-heading-title-font-size: 1.5rem;
--system-subheading-font-size: 1.2rem;
--system-caption-font-size: 0.65rem;


/* Легче просматривать: */
--system-color-controlAccent: blue;
--system-color-focusRing: cadetBlue;
--system-color-labelQuaternary: lightGray;
--system-fontSize-headingTitle: 1.5rem;
--system-fontSize-subheading: 1.2rem;
--system-fontSize-caption: 0.65rem;
}

В нотации триптиха для ограничения количества дефисов используется camelCase. Это позволяет среднему сегменту последовательно указывать тип значения, а последнему сегменту — конкретное имя переменной. На мой взгляд, такое последовательное размещение дефисов делает пользовательские свойства более удобными для быстрого чтения.

Пространство имён

В приведённом выше примере имена переменных имеют префикс 'system' — сокращение от 'design system'. Это называется пространством имён. Имена переменных с распределением имён помогают избежать коллизий, когда CSS используется совместно в нескольких проектах. Другими словами, они помогают избежать ситуаций, когда разработчик вне вашего проекта случайно определяет переменную с таким же именем. Ещё одно преимущество заключается в том, что разделение имён даёт подсказку в веб-инспекторе о том, в каком проекте было определено пользовательское свойство.

Пространство имён в именах переменных может быть важным для глобальных переменных верхнего уровня, но я бы сказал, что этот тип области видимости имён обычно не нужен или полезен, когда переменная определяется ниже верхнего уровня. Это происходит потому, что за вас это делает CSS. Пользовательское свойство всегда ограничивается селектором, в котором оно определено. Если вы определите пользовательское свойство с помощью CSS-селектора для пользовательского элемента с названием quiz-library, то это свойство будет существовать только в узлах DOM, которые соответствуют quiz-library и их дочерним элементам.

:root {
/* Эта переменная определена в корне, что делает её доступной
глобально, поэтому наличие пространства имён полезно. */

--system-color-labelPrimary: #000;
}

quiz-library {
/* Эта переменная определяется внутри библиотеки quiz-library,
поэтому она не доступна глобально. Пространство имён "quizLibrary"
было бы избыточным, потому что переменная доступна только внутри
элементов quiz-library и их потомков. */

--color-questionTitle: var(--system-color-labelPrimary);
}

Типизация значения

В приведённых выше примерах тип значения (color, fontSize и т. д.) указывается в имени пользовательского свойства. Рассмотрите возможность включения информации о типе значения в имена переменных, чтобы поддерживающие код могли понять, какого рода значение хранится в переменной. Это часто называют Венгерской нотацией.

button {
/* Установили ли они определение семейства шрифтов в размера шрифта? */
font-size: var(--system-elephant);
}

button {
/* Теперь ясно, что переменная задаёт размер шрифта. */
font-size: var(--system-fontSize-elephant);
}

Описательные имена

В CSS есть две основные категории имён переменных. Рассмотрим эти две переменные:

  1. --color-icyBlue (value-based)
  2. --color-accent (value-based)

Одна из них обозначена как константа — по названию, "Icy Blue", никогда не должна содержать ничего, кроме значения синего цвета. Другой — более динамичный: конкретный цвет, хранящийся в "Accent", может меняться в зависимости от того, где он используется; например, в каком проекте используется эта переменная.

Люди называют эти категории по-разному. Я буду называть их value-based: имена, описывающие значение, и usage-based: имена, описывающие применение.

Где использовать value-based именование

Переменные с именами, основанными на значениях, могут быть полезны для ограничения количества значений в интерфейсе. Например, хорошим дизайном является ограничение интерфейса небольшим набором цветов. Если в каждой части вашего пользовательского интерфейса используется немного другой оттенок серого, ваш дизайн будет выглядеть непоследовательным и непродуманным. Требование, чтобы каждое использование цвета в вашем интерфейсе было переменной, позволяет ограничить ваши цвета набором, определённым как переменные. Количество размеров шрифтов, их вес, длительность анимации, высота панелей (определяемая отображением их теней) — всё это полезно ограничить.

/* Value-based переменные на глобальном уровне */
:root {
/* Цвета */
--system-color-bondiBlue: rgb(0 58 71);
--system-color-canaryYellow: rgb(255 239 0);
--system-color-caribbeanGreen: rgb(0 204 153);

/* Размеры шрифта */
--system-fontSize-jumbo: 3.052rem;
--system-fontSize-large: 1.563rem;
--system-fontSize-small: 0.8rem;

/* Вес шрифта */
--system-fontWeight-bold: 700;
--system-fontWeight-medium: 400;
--system-fontWeight-light: 200;

/* Длительность */
--system-duration-presto: 60ms;
--system-duration-allegro: 125ms;
--system-duration-andante: 500ms;

/* Повышение */
--system-boxShadow-slightlyRaised: 0 1px 2px 0 rgb(0 0 0 / 10%);
--system-boxShadow-floatingBox: 0 0 30px 0 rgb(0 0 0 / 35%);
}

Цветовые палитры

Многие системы дизайна называют цвета в своей цветовой палитре с числовым суффиксом, чтобы указать контраст с основным фоновым цветом. Считается, что это может быть полезно для потребителей палитры, чтобы иметь возможность легко определить, будет ли тот или иной цвет соответствовать требованиям WCAG по контрастности цвета текста. Это умная идея, но во многих проектах количество цветов, используемых для текста (единственных, которые имеют значение для требований WCAG по контрасту), очень ограничено, поэтому называть всю палитру таким образом только для нескольких цветов может быть излишеством. Кроме того, я не уверен, что эти цифры действительно ускоряют внедрение контрастных пользовательских интерфейсов. Алгоритм контрастности, используемый WCAG, скорее всего, изменится, и существует множество способов преобразовать цвет таким образом, чтобы свести на нет значение числового суффикса. Если вам придётся постоянно перепроверять соотношение контрастности в отрисованном пользовательском интерфейсе, то использование этих чисел не сэкономит времени.

Тем не менее эти числа дают полезную возможность сразу понять, какой цвет светлее или темнее. Хотя я считаю, что использование слов, а не чисел, является более удобным для человека способом достижения этой цели. Рассмотрите возможность использования составных имён для цветовых переменных. Одно имя относится к основному цвету ("red", "yellow", "blue"), а другое выступает в качестве дифференциатора ("cherry", "sunflower", "sky").

:root {
/* Не очень дружелюбен к людям */
--system-color-red400: hsl(0 100% 50%);
--system-color-yellow200: hsl(48 100% 50%);
--system-color-blue300: hsl(200 100% 50%);

/* Более дружелюбный и понятный */
--system-color-cherryRed: hsl(0 100% 50%);
--system-color-sunflowerYellow: hsl(48 100% 50%);
--system-color-skyBlue: hsl(200 100% 50%);
}

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

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

button.destructive {
/* В нашем коде есть UX баг, если этот цвет не красный,
но имя переменной ниже несколько двусмысленно. */

color: var(--system-color-ferrari);
}

button.destructive {
/* Теперь с первого взгляда ясно, что красный цвет был установлен правильно */
color: var(--system-color-ferrariRed);
}

Это соглашение об именовании можно расширить, чтобы включить в него значение альфа канала, хотя это немного сложно. Отдельно от именования, просто в качестве общей практики, часто лучше достигать значений прозрачных цветов в пользовательском интерфейсе через преобразования слоёв или именовать переменные на основе использования, вместо размещения их в статической цветовой палитре. Тем не менее, когда мне нужно было написать имя value-based переменной для прозрачного значения, используя эту конвенцию, я помещал эту информацию в начало имени цвета, используя термины, аналогичные прозрачности в реальном мире.

:root {
--system-color-semitransparentBondiBlue: rgb(0 58 71 / 10%);
--system-color-translucentBondiBlue: rgb(0 58 71 / 30%);
--system-color-frostedBondiBlue: rgb(0 58 71 / 70%);
}

Где использовать usage-based именование

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

В качестве надуманного примера можно назвать вес шрифта, используемого в кнопке, отправляющей форму регистрации. Эту переменную можно было бы назвать как-то вроде --fontWeight-regFormSubmitButton, но это очень специфично. Как правило, все кнопки отправки выглядят одинаково, и в этом случае понятие "вес шрифта кнопки отправки" можно абстрагировать в менее конкретное имя, например --fontWeight-submitButton. Это имя является более общим и, как следствие, более высокого уровня абстракции, поскольку оно больше не ссылается на конкретную форму.

Часто в проекте имеет смысл сочетать переменные с несколькими уровнями абстракции. Вот как это может проявиться в случае с управлением тонированием:

:root {
/* Цветовая палитра определённая в корне */
--system-color-bondiBlue: rgb(0 58 71);
--system-color-canaryYellow: rgb(255 239 0);
}

body {
/* Пользовательское свойство для пользовательских элементов управления */
--color-accentColor: var(--system-color-bondiBlue);
/* Отразите это ниже для встроенных элементов управления */
accent-color: var(--color-accentColor);
}

foobar-custom-control {
/* Определение CSS-интерфейса, позволяющего изменять фон */
--color-background: var(--accentColor);
/* Реализуйте вышеуказанный интерфейс */
background: var(--color-background);
}

form.tinted {
/* Определите CSS-интерфейс для применения цветов оттенка к форме */
--color-formTint: var(--system-color-canaryYellow);
}

form.tinted foobar-custom-control {
/* Используйте вышеуказанный интерфейс для foobar-custom-control */
--color-background: var(--color-formTint);
}

Тёмный режим стал проще с usage-based переменными

Рассмотрим реализацию стилизации тёмного режима без usage-based переменных, и с ними. При использовании только value-based переменных, код гораздо более повторяющийся и многословный.

/* Тёмный режим БЕЗ usage-based переменных… */

:root {
--system-color-deepBlack: #333;
--system-color-offWhite: #eee;
--system-color-skyBlue: lch(33 111 231.17);
--system-color-deepBlue: lch(14 111 231.17);
}

/* Поскольку вышеуказанные переменные названы в value-based
стиле мы не можем обоснованно изменить их значения. Вместо
этого мы переделываем наш CSS ниже, чтобы использовать тот
или иной вариант. */


[data-appearance="light"] body {
color: var(--system-color-deepBlack);
background: var(--system-color-offWhite);
}

[data-appearance="dark"] body {
color: var(--system-color-offWhite);
background: var(--system-color-deepBlack);
}

[data-appearance="light"] a {
color: var(--system-color-deepBlue);
}

[data-appearance="dark"] a {
color: var(--system-color-skyBlue);
}

С помощью usage-based имён, вы можете определить цветовые переменные как концепции интерфейса, которые имеют понятное значение за пределами отдельных частей пользовательского интерфейса, что позволяет изменять их значения для тёмного режима без необходимости поддерживать отдельный светлый/тёмный CSS для каждой новой части пользовательского интерфейса.

/* Тёмный режим С usage-based переменными… */

[data-appearance="light"] {
--system-color-textPrimary: #333;
--system-color-fillPrimary: #eee;
--system-color-link: lch(14 111 231.17);
}
[data-appearance="dark"] {
--system-color-textPrimary: #eee;
--system-color-fillPrimary: #333;
--system-color-link: lch(33 111 231.17);
}

/* С помощью имён usage-based переменных, мы можем изменять
значения для описанных в них вариантов использования на
очень высоком уровне абстракции, позволяя коду нижнего
уровня, использующему переменные, не понимать текущий
режим отображения. */


body {
color: var(--system-color-textPrimary);
background: var(--system-color-fillPrimary);
}

a {
color: var(--system-color-link);
}

Уровни иерархии в usage-based переменных

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

Например, если цвет фона сайдбара и информационных панелей — #eee по отношению к #fff основного фона, возможно, с точки зрения дизайна этот цвет призван донести до пользователя, что пользовательский интерфейс с таким фоном имеет второстепенный характер.

:root {
--system-color-backgroundPrimary: #fff;
--system-color-backgroundSecondary: #eee;
}

Утилитарность usage-based имени, заключается в том, как оно ориентирует разработчика или дизайнера в его применении. Будьте осторожны и не используйте слишком общие имена. Например, --system-color-primary имеет слишком широкое значение и не позволяет понять, где его следует использовать.

/* Не делайте так */

:root {
/* Для чего это?*/
--system-color-box: var(--system-color-neonBlue);
}

:is(a, button, input):focus-visible {
/* Правильно ли используется эта переменная? */
background: var(--system-color-box);
}

/* Вместо этого делайте так */

:root {
/* Понятно, для чего это нужно */
--system-color-focusRing: var(--system-color-neonBlue);
}

:is(a, button, input):focus-visible {
/* Очевидно, что используется правильно */
outline-color: var(--system-color-focusRing);
}

Будьте осторожны и не используйте слишком специфические имена. Например, --system-color-mainToolbarBackground может быть использован только в одном месте, что делает использование переменной излишним.

/* Не делайте так */

:root {
/* Он может быть использован только в одном месте,
а значит, его существование излишне усложняет проект */

--system-color-mainToolbarBackground: #eee;
}

main .toolbar {
background: var(--system-color-mainToolbarBackground);
}

/* Вместо этого делайте так */

:root {
/* Это название является достаточно общим, чтобы
его может использоваться во всех пользовательских
интерфейсах, поэтому оно полезно на глобальном уровне. */

--system-color-backgroundSecondary: #eee;
}

main .toolbar {
background: var(--system-color-backgroundSecondary);
}

Подведение итогов

Можно ещё много чего сказать на эту тему, но я на этом закончу. Пожалуйста, не стесняйтесь обращаться ко мне, e-mail или mastodon. Я с удовольствием продолжу дискуссию!

Дополнительные материалы

Предыдущая Статья

Объектно-ориентированное программирование в JavaScript

Следующая Статья

Ошибки доступности, встречающиеся при проведении аудита