Markup StyleGuide

Руководство по cтилю для написания эффективного, поддерживаемого и маштабируемого кода препроцессора CSS.

Для создания стилевой разметки наших проектов мы, идя в ногу со временем, стремясь к максимально компактному и выразительному коду, используем динамические расширения CSS, препроцессоры Sass (sass-scss.ru) и LESS (less-lang.info). Все примеры в этом руководстве даны в SCSS-синтаксисе Sass.

Именование

  • имена записываются в нижнем регистре, для разделения слов в именах используется дефис (-).

Именнование должно быть:

  • осмысленным;
  • лаконичным.

При именовании идентификаторов мы, как это ни странно, ориентируемся на два, по сути, противоположных подхода:

  1. более методичный формальный БЭМ-подход, стиль Two Dashes:
    
        .block-name__elem-name--mod-name {}
                    
    • имя элемента отделяется от имени блока с помощью двух подчеркиваний (__);
    • модификаторы отделяются с помощью двух дефисов (--).

  2. более гибкий абстрактный и комплексный творческий подход, при котором идентификатор некоего элемента, свойства, функционального качества, который указывает на содержание незначительно меняющееся в большинстве случаев, проще именовать утилитарно, обозначая одним простым классом [без привязки в конкретным шаблонам, блокам и элементам] по всему проекту (например — некая простая анимация, оверлей или текст блока):
    
        .block-name.active,
        .elem-name.on,
        .block-name .overlay,
        .block-name .text {}
                    

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

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

Для того чтобы стало более понятно как это, использовать оба совсем разныx подхода?, приведём простой наглядный пример. Предположим, нам необходимо разметить страницу содержащую два баннера-секции с несколькими простыми элементами внутри: заголовком, параграфом текста и кнопкой перехода. В таком случае нам требуются всего три БЭМ-идентификатора: самой страницы и этих двух секций, во всех остальных случаях, для остального контента мы можем обойтись семантикой голых тегов (h2, p) и простыми классами-утилитами (.btn):


    <body>
        <main class="page page-xxxx" role="main">
            <section class="banner page-xxxx__banner-01">
                <h2>Заголовок 1</h2>
                <p>Текст текст текст текст текст текст текст текст текст текст</p>
                <a href="#" class="btn">Перейти</a>
            </section>
            <section class="banner page-xxxx__banner-02">
                <h2>Заголовок 2</h2>
                <p>Текст текст текст текст текст текст текст текст текст текст</p>
                <a href="#" class="btn">Перейти</a>
            </section>
        </main>
    </body>
        

Шаблон стилей на SCSS может иметь вид:


    // Дефолтные ссылки
    a {
        ...
    }
    // Дефолтная типографика проекта
    h2 {
        ...
    }
    p {
        ...
    }
    // Кнопка - самый общий случай
    .btn {
        ...
    }
    // Баннер - самый общий случай
    .banner {
        ...
    }
    // Страница
    .page {
        ...
    }
    // Конкретный макет
    .page-xxxx {
        ...
        // Первый баннер макета
        &__banner-01 {
            ... // Типографика и кнопка для этого баннера
        }
        // Второй баннер макета
        &__banner-02 {
            ... // Типографика и кнопка для этого баннера
        }
    }
        

Теперь мы можем влиять на поведение простых компонент-утилит и тегов с помощью трёх индентификаторов которые надежно именнованы согласно БЭМ-методу.

Синтаксис (SCSS)

Основное:

  • отступ в четыре (4) пробела, строго никаких табов;
  • cтроки и URL в двойных кавычках;
  • вычисления всегда в круглых скобках;
  • не опускать 0 в начале перед точкой для чисел меньше 1;
  • комментарии для каждого важного селектора, примеси.

Селекторы, правила и примеси:

  • открывающая скобка ({) отделяется от последнего селектора одним пробелом;
  • каждое объявление на собственной новой строке;
  • пробел после двоеточия (:);
  • завершающая точка с запятой (;) в конце всех объявлений;
  • закрывающая скобка (}) на своей новой строке;
  • новая строка после закрывающей скобки (});
  • связанные селекторы на одной строке; не связанные селекторы на новой строке;

    .class-01, .class-01__element,
    .class-02 {
        property-01: value-01;
        property-02: value-02;
        ...
    }
        
  • локальные переменные объявляются перед любыми объявлениями и отделяются от деклараций новой строкой;
  • примешивания растворов и вызовы примесей без @content идут перед любым объявлением;
  • сортировку правил мы осуществляем не по алфавиту, но нестрого ориентируясь на годную концепцию Concentric-CSS;
  • вложенные селекторы и директивы, содержащие собственные объявления, всегда идут после новой строки (но если родительский тег не содержит собственных правил [и мы хотим по каким-то причинам сохранить уровень вложенности], пустую строку перед дочерним селектором можно опустить) в последовательности:
    1. псевдоклассы;
    2. псевдоэлементы;
    3. вложенные селекторы с родительским оператором &;
    4. вложенные дочерние селекторы;
    5. примеси с @content и медиа-запросы (как мы увидим дальше, медиа-запросы в стандартном случае имеют вид такой примеси — мы вкладываем их в сам основной селектор, ипользуя модульный подход, что практически никак не скажется на производительности стилей на production после того как код будет скомпилирован (по сравнению с тем, если бы медиа-запросы были классически скомпонованы в одном месте)).
  • без пробела между именем примеси и первой круглой скобочкой списка аргументов, c пробелом после каждой запятой (перед следующим аргументом) в нём;
  • без новых строк перед закрывающей фигурной скобкой (}).

    .class {
        // Переменные
        $variable-01: variable-01-value;
        $variable-02: variable-02-value;
        $breakpoint-01: breakpoint-01-value;
        // Растворы, простые примеси, правила
        @extend %placeholder-01;
        @include mixin-01($variable-01);
        property-01: value-01;
        property-02: value-02;
        // Псевдоклассы
        &:hover {
            ...
        }
        // Псевдоэлементы
        &::after {
            ...
        }
        // Модификаторы
        &.class-modifier {
            ...
        }
        // Дочерние селекторы
        .class-child {
            ...
        }
        // Примеси с @content и медиа-запросы
        @include mixin-02($variable-01, $variable-02) {
            @content;
        }
        @media (max-width: $breakpoint-01) {
            ...
        }
    }
        

Комментирование

Мы должны стараться оставлять комментарии к каждой переменной (группе переменных), важному селектору/набору правил, примеси. Даже так: всё что не является очевидным [в идеале], должно быть прокомментировано. На самом деле, в реальной практике мы часто имеем дело с проектами, которые практически не содержать комментариев вовсе! (((

  • для комментирования мы используем — ВНИМАНИЕ!!! — только однострочный тип комментария (так они не попадают в скомпилированный код), начинающийся с двух слешей (//);
  • комментарий к селектору идёт на строчке непосредственно перед ним, комментарий к группе селекторов отбивается пустой строкой;
  • комментарий к селектору, группе селекторов, разделу кода располагается на отдельной строке, комментарий к свойству - на той же строке что и комментируемое объявление через пробел после него;
  • комментарий к разделу отбивается двумя пустыми строками сверху, строкой с большим количеством слешей (//////////////////////////////////////////////////////) и пустой строкой снизу.

    // Иконка
    .icon {
        property: value; // комментарий к свойству
        ...
    }
    // Иконки
    .icon-01 {
        ...
    }
    .icon-02 {
        ...
    }
    // Grid and Sizes
    //////////////////////////////////////////////////////
    // Брекпоинт между очень большими экранами и остальными
    $desktop: 1217px;
    // Раствор для усечения и добавления многоточия в слишком длинную строку на одной строке
    // 1. Предотвращает сворачивание содержимого, оставляет его на одной строке.
    // 2. Добавляет многоточие на конце строки.
    %string-overflow-protection {
        white-space: nowrap; // 1
        text-overflow: ellipsis; // 2
        overflow: hidden;
    }
    // Примесь для выставления размера блоку
    // @author Левон Гамбарян
    //
    // @param {Length} $width - ширина элемента
    // @param {Length} $height - высота элемента
    //
    // @example usage:
    // .class {
    //     @include size(100%, 200px);
    // }
    //
    // @example output:
    // .class {
    //     width: 100%;
    //     height: 200px;
    // }
    @mixin size($width, $height) {
        width: $width;
        height: $height;
    }
        

Брекпоинты

Про точки перехода типоразмеров экрана нужно сказать, что обязательно все брекпоинты должны быть стандартизированы для всего проекта в виде глобальных переменных в одном единственном файле _variables.scss (как, впрочем, и все остальные глобальные качества визуального гайдлайна). Например, практически во всех макетах для Rothmans прослеживается одна и та же единая концепция:


    // В файле _variables.scss:
    // Брекпоинт между очень большими экранами и остальными
    $desktop: 1217px;
    // Брекпоинт между гаджетами и остальными экранами
    $mobile: 800px;
        

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


    // В файле _mixins.scss:
    // Media
    @mixin desktop {
        @media (max-width: $desktop) {
            @content;
        } 
    }
    @mixin mobile {
        @media (max-width: $mobile) {
            @content;
        }
    }
        

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


    .wrapper,
    .grid,
    .widget,
    .element,
    .utility {
        ... // Стили для очень больших экранов
        @include desktop {
            ... // Стили для не очень больших экранов и гаджетов
        }
        @include mobile {
            ... // Стили для гаджетов
        }
    }
        

Каскадирование

Невзирая на преобладающее сегодня в среде разработчиков предубеждение, мы очень активно, но при этом аккуратно, используем удобный базовый функционал препроцессора — каскадирование. Если необходимо, и наглядно, в плане выразительности кода препроцессора в development проекте, и технически, в плане содержания и надёжности скомпилированных стилей на production, обозначить зависимость чего-либо от чего-либо — мы вкладываем одно в другое.

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

  • описывает, обслуживает какую-то отдельную определённую функциональность;
  • может быть использовано повторно или модифицировано;
  • независимо, или же — отношения с другими компонентами чётко определены.

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

Короче, возвращаясь к стилям, вкладывать селекторы можно и нужно в следующих основных случаях:

  1. Естественные случаи: конструкции которые нельзя ломать, например, списки и сетки, семантичные голые теги [внутри родительских идентификаторов, без которых они бесполезны] (да и так странно мы тоже иногда поступаем, избегая бездумно плодить лишние классы, перегружая шаблоны HTML и запутывая CSS — см. пункт 3):
    
        #menu {
            ul {
                > li {
                    > a {
                        ...
                    }
                }
            }
        }
        .grid {
            > div {
                ...
            }
        }
                    
  2. Псевдоклассы, псевдоэлементы, состояния-модификаторы, примеси/медиа-запросы для другого представления на разных типоразмерах - для каждого селектора. Используем пресловутый модульный подход — мы категорически стараемся держать всю информацию о каждом компоненте, виджете, элементе — в одном месте, это очень удобно и наглядно.
    
        .class {
            &:hover,
            &::before,
            &.active,
            .parent-selector & {
                ...
            }
            @include mobile {
                ...
            }
        }
                    
  3. Мы вкладываем всё остальное в основные обёртки, классы-абстракции, позволяющие выделять и защищать стили в крупных частях проекта: это различные страницы-шаблоны, темы, версии. Такой подход позволяет нам добиваться большей компактности и переиспользуемости кода, по необходимости легко влиять на поведение нужных более мелких компонентов: конструкций сетки, виджетов, утилит в любом месте проекта.
    
        .template,
        .theme,
        .version,
        .wrapper {
            .wrapper,
            .grid,
            .widget,
            .element,
            .utility {
                ...
            }
        }
                    

Каскадирование используется, уровень вложенности добавляется только в том случае, если он действительно нужен и обозначает отношение, которое необходимо отразить.

Теперь, чтобы всё стало окончательно прозрачно и понятно, приведём простой наглядный пример. Предположим, что в макетах шаблона, который мы уже рассматривали выше, различным образом для каждого блока-баннера меняется типографика в зависимости от типоразмера экрана (да, кнопки для которых меняется типографика и соответсвенно их размер, часто встречаются в макетах для Rothmans!).


    <body>
        <main class="page page-xxxx" role="main">
            <section class="banner page-xxxx__banner-01">
                <h2>Заголовок 1</h2>
                <p>Текст текст текст текст текст текст текст текст текст текст</p>
                <a href="#" class="btn">Перейти</a>
            </section>
            <section class="banner page-xxxx__banner-02">
                <h2>Заголовок 2</h2>
                <p>Текст текст текст текст текст текст текст текст текст текст</p>
                <a href="#" class="btn">Перейти</a>
            </section>
        </main>
    </body>
        

Как лучше всего организовать разметку стилей и медиа-запросы в ней? Наверное никто не захочет делать это вот так — совершенно безобразно — очень много кода и медиа-запросов в нём:


    .page-xxxx {
        &__banner-01 {
            h2 {
                ... // Типографика заголовка этого баннера для очень больших экранов
                @include desktop {
                    ... // Типографика заголовка этого банера для не очень больших экранах и гаджетов
                }
                @include mobile {
                    ... // Типографика заголовка этого банера для гаджетов
                }
            }
            p {
                ...
            }
            .btn {
                ...
            }
        }
        &__banner-02 {
            ...
        }
    }
        

Вот так тоже будет не очень удобно, хотя этот вариант реализует более классический подход с минимумом медиа-запросов в коде:


    .page-xxxx {
        &__banner-01 {
            ... // Типографика этого баннера для очень больших экранов
        }
        &__banner-02 {
            ... // Типографика этого баннера для очень больших экранов
        }
        @include desktop {
            &__banner-01 {
                ... // Типографика этого банера для не очень больших экранов и гаджетов
            }
            &__banner-02 {
                ... // Типографика этого банера для не очень больших экранов и гаджетов
            }
        }
        @include mobile {
            &__banner-01 {
                ... // Типографика этого банера для гаджетов
            }
            &__banner-02 {
                ... // Типографика этого банера для гаджетов
            }
        }
    }
        

Для данного простого примера нужно организовать включение стилей для других типоразмеров вот так — каждый селектор баннера содержит всю информацию о нём в максимально понятной и удобной форме:


    .page-xxxx {
        &__banner-01 {
            ... // Типографика этого баннера для очень больших экранов
            @include desktop {
                ... // Типографика этого баннера для не очень больших экранов и гаджетов
            }
            @include mobile {
                ... // Типографика этого банера для гаджетов
            }
        }
        &__banner-02 {
            ... // Типографика этого баннера для очень больших экранов
            @include desktop {
                ... // Типографика этого баннера для не очень больших экранов и гаджетов
            }
            @include mobile {
                ... // Типографика этого банера для гаджетов
            }
        }
    }
        

Архитектура

Самое важное для нас — это хорошая единая архитектура проектов, о которой мы должны договориться и неукоснительно ей следовать в дальнейшем.

Предлагается принять такую концепцию:

SCSS/

  • utils/
    • _variables.scss
    • _mixins.scss
    • _functions.scss
    • _placeholders.scss
  • core/
    • _normalize.scss
    • _base.scss
    • _typography.scss
    • _grid.scss
    • _utilities.scss
    • _widgets.scss
  • components/
    • _slider.scss
    • _carousel.scss
    • ...
  • layout/
    • _header.scss
    • _menu.scss
    • _footer.scss
    • ...
  • pages/
    • _page-01.scss
    • _page-02.scss
    • ...
  • main.scss

Что важно понимать об этом?

ВНИМАНИЕ!!! У нас есть только один главный файл стилей, который содержит все директивы @import на все остальные стилевые компоненты проекта. Только один, Карл! И больше нигде, ни в одном другом файле данная директива не присутствует! Для проектов с Sass это единственный файл без нижнего подчёркивания в начале имени и в корне папки со стилями main.scss, для проектов на LESS это файл styles.less.


    // Файл main.scss:
    // Utils
    @import "utils/variables"; // все глобальные переменные + переменные для сторонних модулей
    @import "utils/mixins"; // все примеси
    @import "utils/functions"; // все функции
    @import "utils/placeholders"; // все помошники
    // Core
    @import "core/normalize"; // сброс дефолтныых стилей
    @import "core/base"; // основные элементы HTML
    @import "core/typography"; // типографика
    @import "core/grid"; // сетка
    @import "core/utilities"; // простые классы-помощники — утилиты
    @import "core/widgets"; // более сложные составные виджеты-компоненты
    // Components // стили для сторонних модулей!!!
    @import "components/slider";
    @import "components/carousel";
    // Layout // компоненты основного шаблона — крупные конструктивные части общие для всех страниц!
    @import "layout/header";
    @import "layout/menu";
    @import "layout/footer";
    // Pages // стили особые для отдельных страниц
    @import "pages/page-01";
    @import "pages/page-02";
        

А в реальности всё ещё немного сложнее! Мы должны хотя бы надеятся на то, что когда-нибудь абсолютно все сторонние модули и зависимости, используемые нашими проектами, будут — ВНИМАНИЕ!!! — подключаться в проекты не позорно, коряво ручками, а современно, успешно — через пакетный менеджер, например bower. И давайте я не буду здесь объяснять зачем? ((((

Поэтому на самом деле наша сборка стилей будет ещё содержать импорты таких компонент, например так удобно подключать готовый reset:


    // Vendors
    @import "../bower_components/normalize.scss/sass/normalize/import-now"; // готовый reset
    @import "../bower_components/slider/slider.scss"; // некий слайдер
    @import "../bower_components/carousel/carousel.scss"; // некая карусель
    // Utils
    @import "utils/variables"; // все глобальные переменные + переменные для сторонних модулей
    @import "utils/mixins"; // все примеси
    @import "utils/functions"; // все функции
    @import "utils/placeholders"; // все помощники
    // Core
    @import "core/base"; // основные элементы HTML
    @import "core/typography"; // типографика
    @import "core/grid"; // сетка
    @import "core/utilities"; // простые классы-помощники — утилиты
    @import "core/widgets"; // более сложные составные виджеты-компоненты
    // Components // папка для — внимание — кастомизации — сторонних модулей!!!
    @import "components/slider"; // кастомизация некого слайдера
    @import "components/carousel"; // кастомизация некой карусели
    // Layout // компоненты основного шаблона — крупные конструктивные части общие для всех страниц!
    @import "layout/header";
    @import "layout/menu";
    @import "layout/footer";
    // Pages // стили особые для отдельных страниц
    @import "pages/page-01";
    @import "pages/page-02";
        

Далее — ВНИМАНИЕ!!! У нас есть папка utils/, в которой находятся все глобальные переменные представляющие, абстрагирующие на уровень препроцессора визуальный гайдлайн проекта, а также все переменные, которые используют сторонние модули, а также все инструменты и помощники препроцессора в проекте. Каждая глобальная переменная, функция и примесь должна быть помещена сюда!


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

Левон Гамбарян для Castor Digital

05.12.16 — 09.12.16