При создании графических компонентов («виджетов») в первую очередь придумывается их 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/, а о системе вообще – на сайте https://ru.bem.info.
Итого
-
Вёрстка должна быть семантической, использовать соответствующие смыслу информации теги и классы.
-
Класс, описывающий состояние всего компонента, нужно ставить на его корневом элементе, а не на том, который нужно «украсить» в этом состоянии. Если состояние относится не ко всему компоненту, а к его части – то на соответствующем «по смыслу» DOM-узле.
-
Классы внутри компонента должны начинаться с префикса – имени компонента.
Это не всегда строго необходимо, но позволяет избежать проблем в случаях, когда компонент может содержать произвольный DOM, как например диалоговое окно с произвольным HTML-текстом.
Использование
.dialog__title
вместо.dialog .title
гарантирует, что CSS не применится по ошибке к какому-нибудь другому.title
внутри диалога.