Frontender Magazine

Аксиоматический CSS и лоботомированные совы

На последнем CSS Day в июне я с некоторым трепетом представил странный трехсимвольный CSS-селектор. Он назывался «лоботомированная сова» — из-за его сходства с пустым взглядом взглядом этой птицы, и это оказалась самая популярная часть моего доклада.

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

Селектор лоботомированной совы выглядит так:

* + *

Несмотря на непочтительное имя и сомнительную форму, селектор лоботомированной совы для меня не просто игра ума. Это результат продолжительных экспериментов с автоматизацией раскладок с поточным контентом. Совиный селектор — это общеизвестный «жадный» селектор. Ввиду этого многие не решаются его использовать, и кому-то может показаться ужасным то, что я использовал его на продакшене. Но сейчас я хотел бы показать, как этот селектор может упростить код, ускорить процесс разработки и помочь автоматизировать стилизацию произвольного, динамического контента.

Стилизация по умолчанию

Практически все профессиональные дизайнеры веб-интерфейсов (или разработчики, как вам будет угодно) приучили себя по умолчанию стилизировать HTML-элементы. Мы представляем себе элемент интерфейса, а затем создаем стили для объекта, которые вручную привязываем к разметке.

Несмотря на то что он отвечает только за представление, а не за семантику, для стилизации мы чаще всего используем селектор по классу. В то время как элементы и большинство атрибутов предопределены и стандартизированы, классы предоставляют нам свободу творчества. Классы дают нам контроль.

.my-module {
  /* ... */
}

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

<a class="ui-button">Нажми меня</a>

Вне зависимости от того, что вы используете — фреймворк или собственную методологию — такой подход не позволяет подключать к работе редакторов контента, как правило, далеких от верстки. Он требует не только знания разметки, отвечающей за представление, но и доступа к ней, чтобы добавить в код указания нужных стилей. WYSIWYG-редакторов и инструментов вроде маркдауна определенно недостаточно для того, чтобы необходимость стилизации контента не усложняла процесс редактирования.

Разрастание кода

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

Выбрав стилизацию, полностью полагающуюся на элементы, мы ошибочно предполагаем, что HTML-элементы существуют в вакууме, а не являются объектом наследования и не принадлежат своему контексту. Рассматривая элемент как «штуку, для которой надо написать стили», мы наделяем конкретный элемент избыточным значением и определяем для него стили, которые уже должны были быть определены выше в каскаде. Добавление новых модулей в проект приводит к неконтролируемому разрастанию кода.

.module-new {
  /* И … что в нем нового? */
}

Что касается препроцессоров с их одержимостью переменными и объектно-ориентированными CSS-методологиями, переиспользуемыми объектами классов — с их помощью мы сбрасываем балласт, чтобы остановить разрастание кода. Это одержимость нашей индустрии. Однако большинство из них навязывают философию, которая, в первую очередь, и порождает разрастание кода. Некоторые из них даже навязывают плоскую структуру CSS, цитируя решение проблемы веса селекторов — сводя CSS к SS (Каскадные таблицы стилей к просто таблицам стилей. Без использования каскада. Примечание переводчика.) и отказываясь от одного из основных его достоинств.

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

Производительность селекторов

Я допускаю, что некоторые из вас начали неодобрительно качать головами, увидев два астериска в селекторе * + * в начале статьи. Для этого есть причина. Универсальный селектор — это действительно мощный инструмент. И его можно использовать не только во зло, но и во благо. Прежде чем углубиться в это, однако, я хотел бы обсудить проблему быстродействия.

Все исследования, которые мне довелось прочесть, включая исследования Стива Шодерса (Steve Souders) и Бена Фрэйна (Ben Frain), пришли к выводу, что сравнительное быстродействие CSS селекторов разных типов показывает, что разница пренебрежительно мала. Фактически Фрэйн заключает, что «заморачиваться по поводу селекторов в современных браузерах — бессмысленно». И я пока не встречал ни одного убедительного аргумента, опровергающего результаты этих исследований.

Согласно Фрэйну, наоборот, именно количество CSS-селекторов, разрастание кода, может привести к проблемам; особо он упоминает неиспользуемые, хотя и объявленные CSS-правила. Другими словами, выбирать селекторы по классу за их «скорость» бесполезно, если настоящие проблемы с производительностью вызывает их количество. Хорошо, это плюс гигантские JPEG-изображения и веб-шрифты, которые не были урезаны до необходимого подмножества символов.

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

Настоящая проблема с универсальным селектором в том, что он сам по себе не выражает ничего, кроме «стилизуем всё подряд». Фокус в том, как использовать его для создания более сложных селекторов, учитывающих контекст.

Разбираемся с отступами

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

.module-new {
  margin-bottom: 3em; /* что, каждый раз? */
}

Что нам нужно, так это выражение (селектор), которое соответствует только элементам, которым нужен отступ. И это только те элементы, которые находятся в одном контексте с другими, соседними, элементами. Селектор соседа делает именно это: используя форму x + n, мы можем добавить верхний отступ любому элементу n, перед которым есть x.

В обычных условиях эта задача очень быстро приведет к описанию множества возможных случаев, если мы начнем создавать правила для каждой пары соседей. Вот почему мы используем вышеупомянутый универсальный селектор, создавая мордочку совы. Аксиома следующая: «все элементы в потоке, которые расположены после другого элемента, должны получить верхний отступ равный интерлиньяжу».

* + * {
  margin-top: 1.5em;
}

Предрасположенность к ошибкам

Предположим, что размер кегля шрифта параграфов 1 em и интерлиньяж 1.5 em —  устанавливаем отступ в одну строку всем элементам в потоке, которые следуют за другим элементом в любых вариациях и в любом порядке. И мы, разработчики, и ребята, которые создают контент проекта, можем не волноваться о том, что забыли элемент и не прописали использование стандартного отступа, когда он идет после другого элемента. Чтобы добиться этого обычными методами, мы должны для каждого отдельного элемента определять свои отступы. Скучно, долго и всегда есть риск что-то упустить.

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

Учитываем контекст

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

Диаграмма

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

Диаграмма

Это в высшей степени более лаконично и надежно, чем неаксиоматичный подход и необходимость подчищать лишний клей постфактум, как с неохотой признал Крис Койер (Chris Coyier) в «Отступы после модулей». Именно эта статья, надо заметить, подсказала мне идею лоботомированной совы.

.module > *:last-child,
.module > *:last-child > *:last-child,
.module > *:last-child > *:last-child > *:last-child {
  margin: 0;
}

Обратите внимание, что все это работает только в контексте «модуля» (это была большая просьба от редактора контента) и требует оценивать возможную степень вложенности. В данном случае до третьего уровня вложенности.

Дизайн на основе исключений

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

Книжные параграфы с выключкой по ширине

p {
  text-align: justify;
}

p + p {
margin-top: 0;
text-indent: 2em;
}

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

Компактные модули

.compact * + * {
  margin-top: 0.75em;
}

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

Виджеты с позиционированием

.margins-off > * {
  margin-top: 0;
}

Совиный селектор обладает широким спектром действия и будет применяться также к виджетам, например, картам, где позиционирование должно быть точным. Это простой способ его отменить. Все чаще виджеты такого типа реализуются в виде веб-компонент, в которых это правило все равно бы не наследовалось. Это происходит благодаря инкапсуляции стилей в теневом DOM.

Красота em

Однако некоторые исключения неизбежны из-за использования единиц em для определения размера отступа — они уже сами по себе автоматически изменяются из-за другого свойства: @media запросов.

Когда дело касается заголовков, мы получаем некоторые преимущества. Определив кегль шрифта заголовка в em, мы сразу получаем соответствующий заголовку отступ (отступ лида), не написав ни одной дополнительной строки кода.

Диаграмма

Строчные элементы

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

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

Диаграмма

Если вы обнаруживаете, что часто перезаписываете совиный селектор, скорее всего, в вашем дизайне имеются глубокие системные проблемы. Совиный селектор работает с поточным контентом, и такой тип контента должен быть основным на странице. Я бы не советовал сильно полагаться на позиционированный контент в большинстве интерфейсов, так как он нарушает неявное взаимодействие элементов в рамках потока. Даже сетки с их флоат-колонками не должны требовать чего-то большего, чем простой селектор .row > *, применяющий к ним margin-top: 0, чтобы сбросить отступ.

Диаграмма

Заключение

Я плохой математик, но Начала Эвклида произвели на меня большое впечатление: набор тривиальных правил, или аксиом, которые формируют основу для сложного и прекрасного предмета — геометрии. Благодаря Эвклиду я понимаю, что даже наиболее сложные системы должны быть основаны на элементарных правилах, и это также будет верно в отношении CSS. Кроме того, разбиение на модули сложного интерфейса — это необходимый шаг на пути его взросления, и любой интерфейс, который не следует этим базовым принципам, потеряет свою прозрачность.

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

Если вы совсем отключите CSS на странице, вы, возможно, заметите две вещи. Первая — то, что страница упорно сохраняет гибкость: контент укладывается в область просмотра независимо от ее размеров. Вторая — при условии, что вы написали соответствующую стандарту и доступную разметку, вы, наверное, увидите, что контент уже стилизован если и не особо красивым, то хотя бы приемлемым образом. Встроенные стили браузера позаботились и об этом.

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

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Heydon Pickering
Автор:
Heydon Pickering
Сaйт:
http://www.heydonworks.com/
GitHub:
Heydon
Twitter:
@heydonworks
Антон Немцев
Переводчик:
Антон Немцев
Сaйт:
http://frontender.info/
Twitter:
@silentimp
GitHub:
SilentImp
Skype:
ravencry

Комментарии (4 комментария, если быть точным)

Автар пользователя
skaflock

:not(:first-child)

Автар пользователя
SelenIT

@skaflock, а также :nth-child(n+2) :). Но у псевдокласса (как и класса) специфичность выше, чем у тега, поэтому для отмены соотв. правила, скажем, для всех абзацев, понадобится как минимум p:not(:first-child) (уже дублирование). А у «совы» специфичность ноль, так что ее перекроет и обычный p. Насколько я понял, в сочетании нулевой специфичности с полезным действием (пусть и в ограниченной задаче) и состоит основная новизна статьи.

Автар пользователя
codefucker

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

Автар пользователя
mastersam92

По субъективному ощущению, интересно, но в начале и в случае с маленькими страничками.

Когда горы элементов и стилей, всё в итоге всеравно скатывается к кучам селекторов и звёзды в этом деле вместе с прочей уличной магией изрядно подпортят читаемость и осознаваемость написанного