DEV Community

Arif Balaev
Arif Balaev

Posted on

Привет, CSS Cascade Layers (Каскадные слои)

Вольный перевод статьи Hello, CSS Cascade Layers от Ahmad Shadeed

Мой twitter

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

Чтобы преодолеть проблемы с каскадом и специфичностью, нам нужно быть осторожными в местах, где мы пишем CSS. В малых проектах всё может быть в порядке, однако в больших проектах задача может оказаться трудоёмкой. В результате стали появляться различные методы для организации CSS и, соответственно, сокращения проблем с каскадом. Первые три, что приходят мне на ум, это BEM, Smacss от Jonathan Snook и Inverted Triangle CSS от Harry Roberts

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

Проблема

Основная проблема, которую решают каскадные слои, это гарантированный способ написания CSS, не беспокоясь о специфичности и порядке исходного кода. Давайте обратимся к примеру, чтобы проиллюстрировать проблему.

У нас есть кнопка с двумя стилями: default и ghost. В HTML мы будем их использовать следующим образом:

<footer class="form-actions">
    <button class="button">Save edits</button>
    <button class="button button--ghost">Cancel</button>
</footer>
Enter fullscreen mode Exit fullscreen mode

Всё отлично работает в этом случае. Но что, если у нас появился третий вариант кнопки и мы не можем написать его сразу после селектора .button?

.button идёт после .button--facebook. В итоге он будет переопределён. В данном случае можно найти решение через повышение специфичности для .button-facebook, например:

.some-parent .button--facebook {
    background-color: var(--brand-fb);
    color: #fff;
}
Enter fullscreen mode Exit fullscreen mode

Или можно сделать так (не повторяйте это в домашних условиях!):

.button--facebook {
    background-color: var(--brand-fb) !important;
    color: #fff !important;
}
Enter fullscreen mode Exit fullscreen mode

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

Введение в CSS Cascade Layers (каскадные слои)

Каскадные слои это новая CSS фича, которая позволяет разработчикам получать больший контроль над написанием CSS в больших проектах. По словам автора спецификации Miriam Suzanne:

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

Давайте применим каскадные слои для предыдущего примера.

Для начала надо обозначит слой. Чтобы это сделать, мы напишем @layer.

@layer components { }
Enter fullscreen mode Exit fullscreen mode

Я обозначил слой с именем components. Внутри этого слоя мне нужно определить default стили кнопки.

@layer components {
    .button { 
        color: #fff;
        background-color: #d73a7c;
    }
}
Enter fullscreen mode Exit fullscreen mode

Шик. Далее нужно добавить слой для вариаций.

@layer components {
    .button { 
        color: #fff;
        background-color: #d73a7c;
    }
}

@layer variations {
    .button--ghost {
        background-color: transparent;
        color: #474747;
        border: 2px solid #e0e0e0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Вот визуализация слоев. Они аналогичны слоям Photoshop, поскольку то, что определено последним в CSS, будет первым в списке слоев визуально.

В нашем примере variation слой определен последним, поэтому он будет иметь более высокий приоритет, чем слой components.

Существует также другой способ организации того, какой слой переопределяет другой, путем одновременного определения слоев.

@layer components, variations;
Enter fullscreen mode Exit fullscreen mode

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

@layer components, variations;

@layer components {
    .button { 
        color: #fff;
        background-color: #d73a7c;
    }
}

@layer variations {
    .button--ghost {
        background-color: transparent;
        color: #474747;
        border: 2px solid #e0e0e0;
    }

    .button--facebook {
        background-color: var(--brand-fb);
    }
}
Enter fullscreen mode Exit fullscreen mode

Таким образом, мы всегда можем гарантировать, что вариация всегда будет иметь приоритет над default стилями. Давайте рассмотрим объяснение выше визуально.

Обратите внимание, как каждая кнопка живет в слое. Порядок соответствует определению @layer вверху.

Если поменять порядок, то слой components будет переопределять слой variations и default стили "победят".

Добавление стилей в слои

В каскадных слоях браузер комбинирует стили из одних и тех же селекторов @layer и считывает их сразу в соответствии с их порядком.

Рассмотрим следующее:

@layer components, variations;

@layer components {
    .button {..}
}

@layer variations {
    .button--ghost {..}
}

/* 500 lines later */
@layer variations {
    .button--facebook {..}
}
Enter fullscreen mode Exit fullscreen mode

Браузер поместит .button--facebook прямо после .button--ghost в слое variations. Изображение для большей ясности:

Поддержка браузеров

Это самый важный вопрос, чтобы задуматься над новой фичей CSS. Согласно Can I Use на момент написания этой статьи, она поддерживается в Firefox, Chrome, Safari TP. Можем ли мы использовать их в качестве enhancement? Нет, мы не можем. Если только мы не используем полифилл Javascript (которого пока нет).

Где в каскаде живут слои?

Чтобы ответить на этот вопрос, давайте рассмотрим каскад в CSS.

CSS каскад упорядочен следующим образом (первые имеют больший приоритет):

  • Origin и importance
  • Инлайн стилизация
  • Слои
  • Специфичность
  • Порядок в коде Рассмотрим следующий рисунок. Чем толще линия, тем приоритетнее стиль в каскаде.

Origin и importance

Это две разные (но связанные) вещи, так что разберем каждую по-отдельности.

Origin стили приходят из следующих вещей (сначала более приоритетные):

  • Стили разработчика (AKA авторские)
  • Стили пользователя
  • Стили браузера

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

Посмотрим пример.

html {
    font-size: 16px;
}
Enter fullscreen mode Exit fullscreen mode

Если пользователь попробует поменять стили браузера (font-size по-умолчанию в данном примере), то CSS выше всё равно его переопределит, так как стили разработчика приоритетнее пользовательских и браузерных.

Это плохая практика для accessibility (доступности). Пожалуйста, не делайте этого в продакшене. Я просто добавил это ради объяснения origin стилей.

Что касается стилей браузера, они предназначены для user agent стилей. Например, по-умолчанию стиль <button> выглядит по-разному в браузерах. Мы можем его переопределить, как вы уже догадались, потому что стили разработчика имеют преимущество над стилями браузера.

Если вы посмотрите на дефолтные стили кнопки, то увидите user agent stylesheet, которые как раз и показывают стили браузера у кнопки.

Всё вышеописанное касалось стандартных/нормальных/обычных правил. Это значит, что они не имеют ключевого слова !important. В случае, если оно есть, то порядок приоритета будет следующим:

  • Стили браузера с important
  • Стили пользователя с important
  • Стили разработчика с important
  • Обычные стили разработчика
  • Обычные стили пользователя
  • Обычные стили браузера

Инлайн (inline) стили

Если элемент имеет инлайн стили, то у него большая специфичность по сравнению с другими правилами того же importance уровня.

В примере ниже, цвет кнопки будет #fff так как инлайн стили в приоритете.

<style>
button {
    color: #222;
}
</style>

<button style="color: #fff;">Send</button>
Enter fullscreen mode Exit fullscreen mode

От себя: Importance обеих стилей относятся к "Обычные стили разработчика", следовательно далее по уровню проверяется inline. Если бы был у них разный уровень importance, например  color: #222; !important, то это правило отнеслось бы к категории "Стили разработчика с important", что приоритетнее, чем "Обычные стили разработчика". Тогда был бы цвет #222.

Слои

О, привет слои! Это новый гость в каскаде. Каскадные слои имеют больший приоритет, чем специфичность селектора. В следующем примере сможете ли вы угадать размер шрифта элемента p в слое custom?

@layer base, custom;

@layer base {
  #page .prose p {
    font-size: 1rem;
  }
}

@layer custom {
  p {
    font-size: 2rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Шрифт будет 2rem. В каскадных слоях не важна специфичность. Она будет проигнорирована, если элемент переопределен последующим слоем.

Специфичность

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

Вот простой пример. .button внутри .newsletter имеет большую специфичность чем просто .button. В итоге верхнее правило будет переопределено.

.button {
    padding: 1rem 1.5rem;
}

/* Побеждает */
.newsletter .button {
    padding: 0.5rem 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Порядок в коде

Наконец порядок вступает в силу. Когда два элемента имеют одинаковую специфичность, тогда их порядок в документе определяет победителя (тот, что ниже "сильнее").

.newsletter .button {
    padding: 1rem 1.5rem;
}

/* Побеждает */
.newsletter .button {
    padding: 0.5rem 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Теперь у вас есть понимание, где слои находятся в каскаде. Обратимся к нескольким примерам их использования.

Применение каскадных слоёв

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

Переключение UI темы

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

@layer base, elements, objects, components, pages, themes;
Enter fullscreen mode Exit fullscreen mode

У меня есть несколько слоёв и последний это themes. Слой themes может в себе иметь несколько слоев (да, каскадные слои поддерживают вложенность).

Обратите внимание, что вверху я определил @layer custom, default. default будет переопределять custom.

@layer base, elements, objects, components, pages, themes;

@layer themes {
    @layer custom, default;

    @layer default {
        :root {
            --color-primary: #1877f2;
        }
    }

    @layer custom {
        :root {
            --color-primary: #d73a7c;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Если вам потребуется переключить темы, нужно просто поменять порядок слоев в первой строчке @layer themes.

@layer base, elements, objects, components, pages, themes;

@layer themes {
    /* Custom is active */
    @layer default, custom;

    @layer default {
        :root {
            --color-primary: #1877f2;
        }
    }

    @layer custom {
        :root {
            --color-primary: #d73a7c;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Сторонний CSS

Я взял пример, в котором используется карусель flickity. Посмотрите на все ! important значения.

.flickity-page-dots {
    bottom: 20px !important;
}

.flickity-page-dots .dot {
    background: #fff !important;
    opacity: 0.35 !important;
}

.flickity-page-dots .dot.is-selected {
    opacity: 1 !important;
}
Enter fullscreen mode Exit fullscreen mode

С каскадными слоями мы можем добавить сторонний CSS перед слоем компонентов. Мы можем импортировать внешний файл CSS и задать ему слой.

@layer base, vendors, components;

@layer base {
    /* Base styles */
}

/* Import a .css file and assign it to a layer */
@import url(flickity.css) layer(vendors);

@layer components {
    .flickity-page-dots {
        bottom: 20px;
    }

    .dot {
        background: #fff;
        opacity: 0.35;
    }

    .dot.is-selected {
        opacity: 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Меньше переживать об ошибках специфичности

Допустим, у нас есть компонент списка, и нам нужен вариант, в котором список имеет меньший margin.

<ul class="list">
    <li class="list__item list__item--compact">Item 1</li>
    <!-- Other items -->
</ul>
Enter fullscreen mode Exit fullscreen mode

Поскольку псевдоселектор :not придает элементу большую специфичность, его нельзя переопределить без повторного использования :not. Рассмотрим следующее:

/* Побеждает */
.list__item:not(:last-child) {
    margin-bottom: 2rem;
    outline: solid 1px #222;
}

.list__item--compact {
    margin-bottom: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

.list__item--compact не будет переопределять .list__item^ так как последний имеет большую специфичность из-за использования :not. Чтобы все заработало, нам нужно написать следующее:

.list__item:not(:last-child) {
    margin-bottom: 2rem;
    outline: solid 1px #222;
}

.list__item--compact:not(:last-child) {
    margin-bottom: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Посмотрим, как с этой проблемой справятся каскадные слои.

Представим, что @layer list содержит base и overrides слои. В overrides я написал альтернативный способ, и он работал, как и ожидалось, поскольку overrides является последним слоем.

@layer list {
    @layer base, overrides;

    @layer base {
        .list__item:not(:last-child) {
            margin-bottom: 2rem;
        }
    }

    @layer overrides {
        .list__item--compact {
            margin-bottom: 1rem;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Вложенные компоненты

В данном примере у нас есть список действий (лайк, комментарий) для основного элемента социальной ленты и еще один список для каждого комментария.

Иконка у блока элемента ленты имеет размер 24px. В компоненте комментариев размер меньше.

@layer feed, comments;

@layer feed {
  .feed-item .c-icon {
    width: 24px;
    height: 24px;
  }
}

@layer comments {
  .comment__icon {
    width: 18px;
    height: 18px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Обратите внимание, что .feed-item .c-icon имеет большую специфичность, чем .comment__icon, но в этом фишка использования каскадных слоев!

Служебный (утилитный) CSS

Мы привыкли добавлять !important к служебным классам CSS, чтобы они всегда применялись к элементу. С каскадными слоями мы можем разместить слов на последнем месте.

Рассмотрим следующий пример. У нас есть header страницы с утилитным классом p-0. Мы хотим сбросить padding до 0.

<div class="c-page-header p-0">
    <!-- Content -->
</div>
Enter fullscreen mode Exit fullscreen mode

Вот как это выглядит с каскадными слоями.

@layer base, vendors, components, utils;

@layer components {
    @layer page-header {
        .c-page-header {
            padding: 1rem 2rem;
        }
    }
}

@layer utils {
    .p-0 {
        padding-left: 0;
        padding-right: 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Больше подробностей о каскадных слоях

Стили без слоя имеют большую специфичность

Если есть стили CSS, которые не назначены слою, то они будут добавлены к неявному последнему слою.

Рассмотрим следующий пример.

.button {
    border: 2px solid lightgrey;
}

@layer base, components;

@layer base {/* Base styles */}

@layer components {
    .button {
        border: 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

В этом примере правило .button определено без @layer, однако браузер поместит его в неявный слой.

@layer base, components;

@layer base {/* Base styles */}

@layer components {
    .button {
        border: 0;
    }
}

/* Implicit layer */
@layer {
    .button {
        border: 2px solid lightgrey;
    } 
}
Enter fullscreen mode Exit fullscreen mode

Вывод

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

Для дальнейшего изучения

-The Future of CSS: Cascade Layers by Bramus Van Damme
-Getting Started With CSS Cascade Layers by Stephanie Eckles
-Cascade layers are coming to your browser by Una Kravets

Oldest comments (2)

Collapse
 
vit1 profile image
Vitaly Pinchuk • Edited

Желание нарастить специфичность - это самое дурное желание верстальщика, так появляется facepalm-code. Самый первый пример с .button--facebook решается через контекст css-переменных: обычной кнопке свойства указываешь через переменную, тогда селектор контекста .button--facebook{ --button-color: blue } можно размещать как до, так и после селектора .button { color: var(--button-color) }

Collapse
 
kukudim profile image
DKup • Edited

А при изменении порядка слоев, они меняются для всего проекта глобально? Или локально, для скоупа, в котором находятся, как css-переменные?