Вёрстка графических компонентов

При создании графических компонентов («виджетов») в первую очередь придумывается их HTML/CSS-структура.

Как будет выглядеть виджет в обычном состоянии? Как будет меняться в процессе взаимодействия с посетителем?

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

Семантическая вёрстка

HTML-разметка и названия CSS-классов должны отражать не оформление, а смысл.

Например, сообщение об ошибке можно сверстать так:

<div style="color:red; border: 1px solid red">
  Плохая вёрстка сообщения об ошибке: атрибут style!
</div>

…Или так:

<div class="red red-border">
  Плохая вёрстка сообщения об ошибке: несемантический class!
</div>

В обоих случаях вёрстка не является семантической. В первом случае – стиль, а во втором – класс содержат информацию об оформлении.

При семантической вёрстке классы описывают смысл («что это?» – меню, кнопка…) и состояние (открыто, закрыто, отключено…) компонента.

Например:

<div class="error">
  Сообщение об ошибке (error), правильная вёрстка!
</div>

У предупреждения будет класс warning и так далее, по смыслу.

<div class="warning">
  Предупреждение  (warning), правильная вёрстка!
</div>

Семантическая верстка упрощает поддержку и развитие CSS, упрощает взаимодействие между членами команды.

Такая верстка удобна для организации JS-кода. В коде мы просто ставим нужный класс, остальное делает CSS.

Состояние виджета – класс на элементе

Зачастую компонент может иметь несколько состояний. Например, меню может быть открыто или закрыто.

Состояние должно добавляться CSS-классом не на тот элемент, который нужно скрыть/показать/…, а на тот, к которому оно «по смыслу» относится, обычно – на корневой элемент.

Например, меню в закрытом состоянии скрывает свой список элементов. Класс open нужно добавлять не к списку опций <ul>, который скрывается-показывается, а к корневому элементу виджета, поскольку это состояние касается всего меню:

<div class="menu open">
  <span class="title">Заголовок меню</span>
  <ul>
    <li>Список элементов</li>
  </ul>
</div>

Или, к примеру, разметка для индикатора загрузки может выглядеть так:

<div class="indicator loading">
  <span class="progress">Тут показывается прогресс</span>
</div>

Состояние индикатора может быть «в процессе» (loading) или «загрузка завершена» (complete). С точки зрения оформления оно может влиять только на показ внутреннего span, но ставить его нужно всё равно на внешний элемент, ведь это – состояние всего компонента.

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

Возможно и такое, что состояние относится к внутреннему элементу. Например, для дерева состояние открыт/закрыт относится к узлу, соответственно, класс должен быть на узле.

Например:

<ul class="tree">
  <li class="closed">
    Закрытый узел дерева
  </li>
  <li class="open">
    Открытый узел дерева
  </li>
  ...
</ul>

Префиксы компонента у классов

Рассмотрим пример вёрстки «диалогового окна»:

<div class="dialog">
  <h2 class="title">Заголовок</h2>
  <div class="content">
    HTML-содержимое.
  </div>
  <div class="close">Закрыть</div>
</div>

<style>
.dialog {
  background: lightgreen;
  border: lime 2px solid;
  border-radius: 10px;
  padding: 4px;
  position: relative;
}

.dialog .title {
  margin: 0;
  font-size: 24px;
  color: darkgreen;
}

.dialog .content {
  padding: 10px 0 0 0;
}

.dialog .close {
  position: absolute;
  right: 4px;
  top: 4px;
  font-size: 10px;
}
</style>

Диалоговое окно может иметь любое HTML-содержимое.

А что будет, если в этом содержимом окажется меню – да-да, то самое, которое рассмотрели выше, со <span class="title"> ?

Правило .dialog .title применяется ко всем .title внутри .dialog, а значит – и к нашему меню тоже. Будет конфликт стилей с непредсказуемыми последствиями.

Конечно, можно попытаться бороться с этим. Например, жёстко задать вложенность – использовать класс .dialog > .title. Это сработает в данном конкретном примере, но как быть в тех местах, где между .dialog и .title есть другие элементы? Длинные цепочки вида .dialog > ... > .title страшновато выглядят и делают вёрстку ужасно негибкой. К счастью, есть альтернативный путь.

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

Здесь имя dialog, так что все, относящиеся к диалогу, будем начинать с dialog__

Получится так:

<div class="dialog">
  <h2 class="dialog__title">Заголовок</h2>
  <div class="dialog__content">
    HTML-содержимое.
  </div>
  <div class="dialog__close">Закрыть</div>
</div>

<style>
  .dialog { ... }
  .dialog__title { стиль заголовка }
  .dialog__content { стиль содержимого  }
  ...
</style>

Здесь двойное подчёркивание __ служит «стандартным» разделителем. Можно выбрать и другой разделитель, но при этом стоит иметь в виду, что иногда имя класса может состоять из нескольких слов. Например title-picture. С двойным подчёркиванием: dialog__title-picture, очень наглядно видно где что.

Есть ещё одно полезное правило, которое заключается в том, что стили должны вешаться на класс, а не на тег. То есть, не h2 { ... }, а .dialog__title { ... }, где .dialog__title – класс на соответствующем заголовке.

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

Без фанатизма

На практике из этих правил зачастую делают исключения. Можно «вешать» стили на теги и использовать CSS-каскады без префиксов, если мы при этом твёрдо понимаем, что конфликты заведомо исключены.

Например, когда мы точно знаем, что никакого произвольного HTML внутри элемента (или внутри данного поддерева DOM) не будет.

БЭМ

Описанное выше правило именования элементов является частью более общей концепции «БЭМ», которая разработана в Яндексе.

БЭМ предлагает способ организации HTML/CSS/JS в виде независимых «блоков» – компонент, которые можно легко перемещать по файловой системе и между проектами.

Можно как взять часть идеологии, например систему именования классов, так и полностью перейти на инструментарий БЭМ, который даёт инструменты сборки для HTML/JS/CSS, описанных по БЭМ-методу.

Более подробное описание основ БЭМ можно почитать в статье https://ru.bem.info/articles/bem-for-small-projects/, а о системе вообще – на сайте http://ru.bem.info.

Итого

  • Вёрстка должна быть семантической, использовать соответствующие смыслу информации теги и классы.

  • Класс, описывающий состояние всего компонента, нужно ставить на его корневом элементе, а не на том, который нужно «украсить» в этом состоянии. Если состояние относится не ко всему компоненту, а к его части – то на соответствующем «по смыслу» DOM-узле.

  • Классы внутри компонента должны начинаться с префикса – имени компонента.

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

    Использование .dialog__title вместо .dialog .title гарантирует, что CSS не применится по ошибке к какому-нибудь другому .title внутри диалога.

Задачи

важность: 5

Посмотрите на вёрстку горизонтального меню.

<div class="rounded-horizontal-blocks">
  <div class="item">Главная</div>
  <div class="vertical-splitter">|</div>
  <div class="item">Товары</div>
  <div class="item">Фотографии</div>
  <div class="item">Контакты</div>
</div>
/*+ hide="Результат со стилями (показать стили)" */
.rounded-horizontal-blocks .item {
  float: left;
  padding: 6px;
  margin: 0 2px;
  border: 1px solid gray;
  border-radius: 10px;
  cursor: pointer;
  font-size: 90%;
  background: #FFF5EE;
}

.vertical-splitter {
  float: left;
  padding: 6px;
  margin: 0 2px;
}

.item:hover {
  text-decoration: underline;
}

Что делает эту вёрстку несемантичной? Найдите 3 ошибки (или больше).

Как бы вы сверстали меню правильно?

Открыть песочницу для задачи.

Несмотря на то, что меню более-менее прилично отображается, эта вёрстка совершенно не семантична.

Ошибки:

  1. Во-первых, меню представляет собой список элементов, а для списка существует тег LI.

    Семантический подход – это когда теги используются по назначению. Для элементов списка <li>, для адреса <address>, для заголовка таблицы <th> и т.п.

  2. Во-вторых, класс rounded-horizontal-blocks показывает, что содержимое должно быть оформлено как скругленные горизонтальные блоки. Любой класс, отражающий оформление, несемантичен.

    Правильно – чтобы класс был смысловым. Например, <ul class="menu"> будет говорить о том, что смысл элемента – «меню».

  3. В-третьих, элемент .vertical-splitter. Здесь класс вполне семантичен, этот элемент списка является вертикальным разделителем, так что здесь всё в порядке. Но на этот раз несемантичность – в содержимом.

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

    Поэтому от него следует либо вообще избавиться, либо переместить в CSS при помощи ::before.

И, наконец, это не обязательно и не ошибка, но обычно элементы, которые являются ссылками или кнопками, оформляют в <a> или <button>.

Вариант ниже – семантичен:

<ul class="menu">
  <li class="menu__item"><a href="#">Главная</a></li>
  <li class="menu__vertical-splitter"></li>
  <li class="menu__item"><a href="#">Товары</a></li>
  <li class="menu__item"><a href="#">Фотографии</a></li>
  <li class="menu__item"><a href="#">Контакты</a></li>
</ul>

Дополнительно, классы помечены префиксом компонента, на тот случай, если в заголовках появится произвольный HTML.

Открыть решение в песочнице.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.