Правила для стилей (CSS, Stylus)

Для описания CSS в веб-приложениях мы используем препроцессор Stylus в сочетании с библиотекой nib. Также используется Autoprefixer, так что писать вручную browser-specific префиксы не нужно.

Оптимизация стилей (упрощение “жизни” браузеру)

В MDN (Mozilla Developer Network) есть замечательная статья о том, как писать эффективный CSS — Writing efficient CSS — которая настоятельно рекомендуется к прочтению. Если коротко, то можно выделять 2 главных правила:

  • лучше использовать селекторы по названию классов и не использовать более “общие” селекторы (по названию тегов, по атрибутам и т.д.),
  • следует максимально избегать селекторов с наследованием, особенно неограниченной глубины (без знака >), вместо этого следуеть испольльзовать наследование свойств CSS от родительского элемента к дочерним.

Этими правилами мы будем руководствоваться для выработки принципов правильной организации CSS.

Принципы правильного выбора CSS-селекторов

  • Главный способ задавать CSS-свойства элементов — CSS-класс, прописанный непосредственно у элемента. Никакие сложные селекторы (вложенные, название тега с классом и т.п.) в общем случае применять не нужно, поскольку они ухудшают производительность и читаемость.
  • Селекторы по названию элемента (тега) в общем случае запрещены. Исключение можно делать:
    • в рамках глобального фреймворка (например, стилизовать все теги заголовков h1, h2,...),
    • если необходимо кастомизировать вид внешнего (не своего) компонента, который нельзя кастомизировать иначе. В этом случае следует использовать максимально узкий селектор с использованием ближайшего элемента с CSS-классом и неглубокого наследования (с использованием >).
  • Селекторые по идентификатору (#) вообще не следует использовать.
  • Непосредственное присвоение CSS-свойств элемента через style сделует применять только в случае динамического характера значений свойств.

Именование CSS-классов

У нас используется css-modules, для того, чтобы избегать конфликты имён CSS-классов, объявленных в разных классах, и случайных переопределений стилей.

В связи с этим, для локальных CSS-классов компонентов следует выбирать лаконичные понятные имена (например, title, add-button, footer) без использования каких-либо префиксов, чтобы обеспечить их уникальность. Добавление префиксов, обеспечивающих глобальную уникальность имени, происходит на этапе сборки проекта автоматически.

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

  • Для внутренней библиотеки придётся использовать отдельный instance classNames (cx1). Впрочем, классы внутренних библиотек должны содержать префикс lc- и существует риск конфликта имён между двумя библиотеками.
  • Класс из внешней библиотеки можно будет использовать только вручную без использования classnames или посредством отдельного unbinded инстанса classnames.

Какие бывают файлы стилей

Основной stylus-файл компонента

Объявляет набор классов, которые используются только в данном компоненте.

Располагается в папке компонента (для компонентов-пакетов) или в папке style/ рядом с файлом компонента, называется так же, как и компонент, но со строчной буквы и с расширением .styl.

Должен подключаться явно в файле компонента:

Для использования в компоненте библиотеку необходимо явно подключить в основном файле компонента:

SomeComponent.tsx
1
2
3
4
5
import classNames from 'classnames/bind'

const cx = classNames.bind(require('./style/someComponent.styl'))

const className = cx('item-label')

Дополнительные stylus-файлы компонента

Могут понадобится, если основной файл разросся и удобно выделить какие-то куски в отдельные файлы.

Располагаются рядом с основным stylus-файлом компонента.

Могут подключаться двумя способами:

  • С помощью директивы @require в основном stylus-файле компонента. Это удобно, если дополнительный файл понадобился только для того, чтобы разбить большой файл.

  • Прямым подключением с помощью classNames.bind наряду с осноным stylus-файлом в файле компонента. Это удобно, если дополнительный файл является мини-библиотекой и используется в нескольких соседних компонентах.

    web/master/public/components/SomeComponent.tsx
    1
    2
    3
    4
    5
    const cx = classNames.bind(Object.assign(
      {},
      require('./style/someComponent.styl')
      require('./style/addForm.styl'),
    ))
    

Общие stylus-библиотеки

Помогают убирать дублирование кода. Они не содержат конечных классов, но определяют переменные и базовые наборы свойств, используемые в нескольких других местах (общих библиотеках классов и стилях компонентов).

Располагается в папке style любого модуля приложения в зависимости от области применимости.

Подключаются в с помощью директивы @require в stylus-файлы внутренних библиотек или компонентов:

web/master/public/components/style/someComponent.styl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@require 'web/master/style/webMasterVars.styl'
@require 'publicVars.styl'    // web/master/public/components/style/publicVars.styl

.some-element
  font-size: primaryFontSize
  line-height: publicTitleHeight

.title
  @extends $title
  font-weight: bold

Можно подключать файлы либо относительно текущей папки (без использования ./), либо любой файл относительно папки src/ (без использования начального /).

Предупреждение

Обратите внимание, что файлы, подключаемые с помощью директивы @require в другие stylus-файлы, не должны содержать в себе объявления конечных css-классов, иначе это приведёт к дублированию этих классов в каждом файле, куда была подключена эта библиотека.

Файлы, содержащие объявления конечных css-классов, должны подключаться только напрямую в компонентах, где эти классы будут использоваться.

Общие библиотеки классов приложения (внутренний CSS-фреймворк)

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

Следует располагать в папке style одного из корневых/общих модулей приложения (web, web/master и т.п.).

Классы внутренних библиотек прогоняются через css-modules при сборке и защищены о того, чтобы конфликтовать или переопределять свойства одноимённых классов из других библиотек.

Для использования в компоненте библиотеку необходимо явно подключить в основном файле компонента:

SomeComponent.tsx
1
2
3
4
5
6
7
8
9
import classNames from 'classnames/bind'

const cx = classNames.bind(Object.assign(
  {},
  require('web/master/style/webMasterLib.styl'),
  require('./style/someComponent.styl')
))

const className = cx('lc-caption', 'item-label')

Чтобы избежать конфликта имён с локальными css-классами компонента, библиотечные классы должны содержать префикс lc- (или более специфичный, если есть риск конфликта имён между разными библиотеками классов).

Внешние CSS библиотеки/фреймворки

Готовые библитеки для создания красивых UI, типа Bootstrap.

Подключаются статически прямо в заголовках index.html приложения (например, src/apps/master/static/index.html) либо из публичных CDN, либо из той же папки static приложения. Классы фреймворка, подключенные таким образом, могут использоваться в любом компоненте приложения без явного импорта (что не очень хорошо с точки зрения целостности и явности, но очень удобно).

Классы из внешних библиотек появляются в глобальном пространстве имён as-is.

Предупреждение

Если в собственных стилях компонента (используемых через classnames) объявлен класс с именем, совпадающим с именем класса в статически подключённой библиотеке, то класс из внешней библиотеки невозможно использовать посредством функции cx, поскольку будет подставлено преобразованное посредством css-modules имя локального класса.

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

Принцип разделения ответственности между родительским и дочерним компонентом

Во многих случаях, когда в структуре компонента используется другой (дочерний) компонент, родительскому компоненту “нужно” каким-либо образом спозиционировать дочерний относительно других своих элементов. Для этого ему нужно задать стили для корневого элемента дочернего компонента. Однако это является своего рода нарушением идеальной картины мира, когда каждый компонент задаёт стили только для своих собственных элементов и не вмешивается в стили других компонентов.

Разрешить это противоречие поможет взгляд на корневой элемент дочернего компонента как на некий кастомный html-тег в рамках родительского компонента. Ведь родительский компонент волен задавать стили для всех “не-компонентных” тегов в своём шаблоне. А чтобы не родительский компонент не вмешивался в детали внутренней реализации дочернего, представим, что этот кастомный html-тег поддерживает только ограниченный набор CSS-свойств, которые определяют позиционирование.

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

  • Родительский компонент имеет право задавать стили только для корневого тега дочернего путём присвоения дополнительного CSS-класса дочернему компоненту. Этот класс объявлен должен быть объявлен в пространстве имён (или зависимостях) родительского компонента, дочерний компонент о нём ничего не “знает”.

  • Для этого дочерний компонент должен принимать свойство className, значение которого должно добавляться к списку классов корневого элемента.

  • Родительский компонент имеет право задавать для корневого тега дочернего компонента только свойства, влияющие на позиционирование, такие как:

    • position. Однако нельзя переопределять значение на position: static, поскольку это может повлиять на отображение элементов внутри дочернего компонента, если в нём было задано значение absolute или relative.
    • z-index
    • margin
    • top, bottom, left, right
  • Если дочерний компонент использует вышеуказанные свойства для своего корневого тега, то желательно описать эти особенности в документации и указать, какие возможности по позиционированию этого компонента предусмотрены.

    Предупреждение

    Это важно также с точки зрения приоритета переопределения свойств. Если в css-классе, добавленном родительским компонентом, и внутреннем классе дочернего компонента, определены разные значения одного и того же свойства, то приоритет определяется не позицией класса в списке классов DOM-элемента, а тем, в каком порядке будут расположены стили системой сборки (webpack), т.е. в общем случае — неопределённом.

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

  • Все другие модификации внешнего вида дочернего компонента должны поддерживаться внутри самого дочернего компонента и управляться снаружи с помощью обычных свойств (props) компонента, отличных от className. Например:

    ParentComponent.tsx
    1
    2
    3
    <div>
      <SomeComponent highlighted />
    </div>
    
    SomeComponent.tsx
    1
    2
    3
    <div className={cx({ highlighted: props.highlighted })}>
      some content
    </div>
    

Форматирование styl-файлов

  • Отступы — 2 пробела (никаких табов).
  • После селекторов двоеточие не ставим.
  • Между названием свойства и значением — двоеточие и пробел.
  • Одно свойство — одна строка. Точки с запятыми не используем.
  • Выравнивать значения свойств в столбик не нужно.
  • 0 без единцы измерения.
  • Названия CSS-классов: .camelCase.
  • Между селекторами — одна пробельная строка.

Другое

!important можно использовать только там, где это действительно нужно. Злоупотреблять запрещено.