Модульный CSS и селекторы атрибутов

Несколько месяцев назад мне попалась статья Гарри Робертса в которой он представил интересную концепцию работы с сопряженными классами в CSS. В этой статье он предложил использовать символы [ ] для группировки классов для того чтобы быстро распознать их смысл при беглом просмотре. Он приводит данный пример, утверждая что в таком виде декларация стилей становится более "распознавабельной":

<div class="[ foo  foo--bar ]  [ baz  baz--foo ]">

Признаюсь, сначала я принял такой подход в штыки. Идея имен классов с именами [ ] которым к тому же не соотвествуют никакие стили, да еще повторяются внутри одного атрибута, которые имеют смысл только для человека и не для браузера, выглядит очень странной. Я собственно и по прежнему думаю также, но тем не менее, это заставило меня задуматься о верстке и семантике поглубже, поэтому - спасибо Гарри!

Пока я думал об этом я нашел что разные люди предлагали похожие подходы, например использовать / (Ben Everard), или | (Steven Nolan), но все эти способы оставляют ощущение искусственности.

Как так вышло что у вас так много классов что вам нужны новые классы чтобы сделать их читабельными?

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

Больше классов против меньше классов

Странная штука, но несмотря на то что обилие классов в разметке кажется мне не нормальным, люди типа Гарри чертовски убедительны. Исходя из принципов OOCSS или Single Responsibility Principle, а также из моего личного опыта работы со сложными сайтами, я могу сказать что есть определенная ценность в "дроблении" стилей, но тем не менее только недавно я нашел способ который меня удовлетворил.

Перед этим я адаптировал версию методологии БЭМ которая делает акцент на изоляции а не на повторном использовании - каждый новый блок по умолчанию не наследует никаких стилей, позволяя компоненту разрабатываться изолированно и избежать риска испортить что либо в другом месте сайта. Но недостаток такого подохода в том что в итоге вы обнаруживаете что у вас 10 различных стилей для ссылок, 12 оттенков голубого, 18 слегка отличающихся стилей кнопок. Николь Саливан, создатель OOCS, в своей фантастической презентации в прошлом году в Мельбурне рассказала как часто такое случается и как это исправлять.

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

<a class='btn large rounded'>
.btn { /* button styles */ }
.large { /* global large-type modifier */ }
.rounded { /* global rounded-border modifier */ }

имеем такое:

<a class='btn btn--large btn--rounded'>
.btn { /* button styles */ }
.btn--large {
  @extend %large-type;
}
.btn--rounded {
  @extend %rounded-borders;
}

В итоге я пришел к тому что у меня была кучка файлов типа _typography.scss, _brand.scss, которые позволяли мне более менее поддерживать фрагментацию и в то же время изоляцию отдельных компонентов. И все было ОК, до поры до времени.

Модификаторы: как М ломает БЭМ

Если вы изучаете вопрос наименования классов в CSS и поддержки стилей, вы неизбежно натолкнетесь на отличную статью Николаса Галахера "About HTML semantics and front-end architecture". Одна часть особенно привлекла мое внимание, то что он называет шаблоном "один класс" против "множественные классы". В вашей разметке потенциально может быть два таких варианта:

<a class='btn--large'> <!-- Single class -->
<a class='btn btn--large'> <!-- Multi class -->

Это соответствует двум подходам в CSS:

/* Single class */
.btn, .btn--large { /* base button styles */ }
.btn--large { /* large button styles */ }

/* Multi class */
.btn { /* base button styles */ }
.btn--large { /* large button styles */ }

Разница тут в том самостоятелен ли класс btn--large или он требует присутствия родительского класса btn. В шаблоне "один класс" он самостоятелен, это выглядит проще и удобнее, и страхует от случая когда вы забыли вписать второй класс. Плюс с функционалом препроцессоров @extend это выглядит совсем просто. Но, такой подход подвержен серьезному недостатку.

Контекстные модификации

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

header > nav > .btn { background: none; }

В случае если вы используете паттерн с одним классом вы не знаете точно какой вариант кнопки вам надо перекрыть поэтому мы вынуждены поступать так:

header > nav {
  .btn, .btn--large, .btn--rounded { background: none; }
}

Как можно догадаться это не идеально, каждый раз когда вы добавляете новый модификатор кнопки вам надо не забыть и вписать сюда этот класс. Это все не очень здраво, поэтому многие считают что надо вернуться к варианту множественных классов. (Nicholas Gallagher, Ben Smithett). Встречал и другие альтернативные варианты, метод Томми Маршала или Бена Фрейна, которые используют селектор атрибута ^= которым можно проверить начинается ли атрибут определенной строкой, например:

<a class='btn--large'>
[class^='btn'] { /* базовые стили кнопок */ }
.btn--large { /* модификатор для большой кнопки */ }
header > nav > [class^='btn'] { /* Перекрывает все кнопки */ }

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

Я конечно ценю изобретательность такого подхода, но это тупик. Это как раз то место где я застрял, до тех пор пока мне не пришло в голову кое что.

Какого

[class^='btn'] { /* base button styles */ }
.btn--large { /* large button styles */ }
header > nav > [class^='btn'] { /* Overrides for all buttons */ }

Это конечно удобный вариант для метода "один класс" но все таки это слишком хрупкая конструкция чтобы быть серьезной альтернативой. Если какой то другой класс окажется перед btn--large то вся конструкция летит к чертям. Плюс ко всему нет ясного способа как в этом случае иметь дело с модификатором модификатора типа btn--large--rounded.

Я ценю изобретательность этого метода но это тупик. И это то место где я застрял до момента когда кое что пришло мне в голову.

А чего мы вообще приклеились
к атрибуту class?

Пардон за мою тупость но кто нибудь может доказать мне что class это единственный атрибут который мы можем использовать для селекторов стилей? Вот что говорит спецификация HTML:

3.2.5.7 The class attribute

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

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

Ну да, это чудесно что мы используем атрибут класс для описания "природы содержимого", но все таки ощущение такое что мы хотим от него больше чем он может дать. Этот многострадальный атрибут содержит все, начиная от громоздких БЭМ имен типа primary-nav__sub-nav--current или вспомогательных классов типа u-textTruncate или left clearfix, до джаваскрипт указателей js-whatevs, в итоге мы тратим кучу времени на придумывание имен которые не конфликтуют один с другим и тем не менее читабельны.

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

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

Магический селектор ~=

Оказывается что браузеры аж с времен IE7 поддерживают мощнецкое правило называемое селектор атрибутов разделенных пробелом, описанную тут на CSS Tricks. Он цепляет произвольные значения атрибутов, разделенных пробеламы, так как будто это и есть классы. К примеры следующие две строки идентичны:

.dat-class { /* dem styles */ };
[class~='dat-class'] { /* dem styles */ };

Точно также как для <div class="a b c "> не имеет значения в каком порядке стоят a, b или с, или что еще есть помимо них, также это не важно для селектора ~=. Но этот селектор не ограничен атрибутом class, он работает точно также с любым другим атрибутом. И это ключевой момент в новом подходе.

"Модули атрибутов", или АМ, это по сути определение пространства имен в которых живут стили. Начнем с простого примера, колонки:

<div class="row">
    <div class="column-12">Full</div>
</div>
<div class="row">
    <div class="column-4">Thirds</div>
    <div class="column-4">Thirds</div>
    <div class="column-4">Thirds</div>
</div>
.row { /* max-width, clearfixes */ }
.column-1 { /* 1/12th width, floated */ }
.column-2 { /* 1/6th width, floated */ }
.column-3 { /* 1/4th width, floated */ }
.column-4 { /* 1/3rd width, floated */ }
.column-5 { /* 5/12th width, floated */ }
/* etc */
.column-12 { /* 100% width, floated */ }

Теперь построим это же самое на АМ. У нас есть два модуля, строки(rows) и колонки(columns). У строки нет вариаций, у колонок их 12.

<div am-Row>
    <div am-Column="12">Full</div>
</div>
<div am-Row>
    <div am-Column="4">Thirds</div>
    <div am-Column="4">Thirds</div>
    <div am-Column="4">Thirds</div>
</div>
[am-Row] { /* max-width, clearfixes */ }
[am-Column~="1"] { /* 1/12th width, floated */ }
[am-Column~="2"] { /* 1/6th width, floated */ }
[am-Column~="3"] { /* 1/4th width, floated */ }
[am-Column~="4"] { /* 1/3rd width, floated */ }
[am-Column~="5"] { /* 5/12th width, floated */ }
/* etc */
[am-Column~="12"] { /* 100% width, floated */ }

Первое что вы наверно заметили это префикс am-. Это важно чтобы не создать конфликтов с другими существующими атрибутами. Конечно можно использовать любой префикс, я пробовал ui-, css- и другие, но оставился на am- для данных примеров. Если валидность HTML критична для вас то можно использовать атрибут в виде data-, смысл тот же.

Второе что вы наверно заметили это значения атрибутов типа 1, 2. Если бы это были имена классов это было бы ужасно, они слишком общеупотребительные и шансы конфликтов велики. Но поскольку мы определили свое собственное пространство имен, мы можем использовать максимально короткие имена.

Гибкость значений атрибутов

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

<div am-Row>
    <div am-Column>Full</div>
</div>
<div am-Row>
    <div am-Column="1/3">Thirds</div>
    <div am-Column="1/3">Thirds</div>
    <div am-Column="1/3">Thirds</div>
</div>
[am-Row] { /* max-width, clearfixes */ }
[am-Column] { /* 100% width, floated */ }
[am-Column~="1/12"] { /* 1/12th width */ }
[am-Column~="1/6"] { /* 1/6th width */ }
[am-Column~="1/4"] { /* 1/4th width */ }
[am-Column~="1/3"] { /* 1/3rd width */ }
[am-Column~="5/12"] { /* 5/12ths width */ }
/* etc */

Теперь мы можем легко использовать имена для наших конкретных нужд, ширина колонки 1/3 сразу же говорит что это значит, тогда как например 4 предполагает что мы помним что используем 12 колоночную сетку. Атрибут column без значения например означает колонку 100% ширины, и как бонус мы можем в атрибут без значения перенести всю повторяемую логику, (float: left) например.

Определение стилей для атрибута и для его значений

Это главная прелесть этого подхода. Наличие атрибута, например am-Button, само по себе может быть застилено. Конкретное значение атрибута модифицирует эти базовые стили.

В примере с сеткой выше мы делаем именно это: разметка am-Column = "1/3" подпадает сразу под оба селектора, и [am-Column] и [am-Column~="1/3"] , поэтому результат это базовые стили плюс вариации. Это избавляет от копирования классов или использования функционала @extend.

"Без классовый" подход к БЕМ модификаторам

Вернемся к спору между шаблоном "один класс" и "множественные классы". АМ предлагает способ без классов. Ну к примеру, для случая четырех вариантов кнопок разметка выглядит так:

<a am-Button>Normal button</a>
<a am-Button='large'>Large button</a>
<a am-Button='rounded'>Rounded button</a>
<a am-Button='large rounded'>Large rounded button</a>
[am-Button] { /* base button styles */ }
[am-Button~="large"] { /* large button styles */ }
[am-Button~="rounded"] { /* round button styles */ }

При помощи атрибута am-Button, мы можем разделить стили которые общие для всех кнопок и стили которые делают кнопку большой, или стили для скругленной кнопки. Мы можем не только спокойно комбинировать вариации кнопок (am-Button = "large rounded"), но мы можем выбирать сам атрибут для любых контекстных модификаций:

header > nav > [am-Button] { background: none; }

Не важно какой вариант кнопки у нас используется, или как много вариантов мы решим создать, мы можем выбрать все кнопки одним селектором [am-Button], поэтому мы можем быть уверены что модификации будут валидными.

AMCSS проект

Я (Glen Maddern), Ben Schwarz и Ben Smithett начали работу над спецификацией АМ. Если вы хотите узнать больше как эта техника расширяет блоки, элементы, точки перехода (breakpoints) и больше, поднимайте пожалуйста свой вопрос.

Также мы сделали сайт с документацией, с разными примерами как использовать АМ, amcss.github.io. Если вам интересно внести свой вклад, сделать примеры или вы хотите показать нам свои варианты АМ библиотек, пишите нам на Гитхаб.

comments powered by Disqus