Разрешите представиться, Каскадные слои CSS

alexei09/10/2024 - 08:31
Разрешите представиться, Каскадные слои CSS

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

Чтобы преодолеть проблемы с каскадом и специфичностью, нам нужно быть осторожными с тем, где писать конкретный блок кода CSS. В небольших проектах все может быть нормально, но для больших это отнимает много времени. В результате мы начали использовать различные методы, помогающие лучше организовать наш код CSS и, таким образом, уменьшить возможные проблемы с каскадом. Первые три, которые сразу приходят в голову, - это BEM (блок, элемент, модификатор), Smacss от Джонатана Снука и Перевернутый треугольник CSS от Гарри Робертса.

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

Итак, приступим.

Основная проблема

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

Основная и производная кнопки

У нас есть кнопка с двумя стилями: по умолчанию и производный. Вот так это будет использоваться в HTML:


<footer class="form-actions">
  <button class="button">Save edits</button>
  <button class="button button--ghost">Cancel</button>
</footer>

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

Конфликт при добавлении третьего варианта кнопки

Селектор .button стоит сразу после нового селектора .button--facebook. В результате новый селектор будет переопределен. Здесь мы могли бы обойти это увеличив специфичность селектора .button--facebook следующим образом:


.какойто-предок .button--facebook {
  background-color: var(--brand-fb);
  color: #fff;
}

Либо мы можем сделать так (в реальности старайтесь никогда так не делать!):


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

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

Введение в каскадные слои CSS

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

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

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

Прежде всего нужно определить слой. Для этого нам необходимо записать его имя, которое следует за декларацией @layer:


@layer components {
}

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


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

Отлично. Теперь нам нужно добавить еще один слой для вариантов кнопки. Назовем его variations.


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

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

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

Визуализация слоев CSS

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

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


@layer components, variations;

Вернемся к нашему примеру. Первоначальная проблема заключалась в том, что нам нужно было создать другую вариацию кнопки, но мы добавляли ее в место, где у нее была более низкая специфичность. С помощью каскадных слоев мы можем добавить наш CSS код в слой 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);
  }
}

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

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

Расположение кнопок в слоях

При изменении порядка слоев слой components переопределит слой variations. В результате победит стиль кнопок по умолчанию.

Другое расположение кнопок в слоях

Добавление правил стиля в слои

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

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


@layer components, variations;

@layer components {
  .button {
    ..;
  }
}

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

/* 500 строк спустя */
@layer variations {
  .button--facebook {
    ..;
  }
}

Здесь браузер добавит селектор .button--facebook сразу после селектора .button--ghost в слое variations. Вот для наглядности иллюстрация:

Как браузер читает слои CSS

Поддержка браузерами

Поддержка браузерами наиболее важный вопрос, от ответа на который зависит решение о практическом использовании новой функции CSS. Согласно сервиса Can I Use и на момент написания этой статьи слои поддерживаются браузерами Firefox, Chrome, Safari.

Можем ли мы без опаски использовать слои CSS в качестве улучшения? Ответ - с большой осторожностью.

Где в каскаде находятся каскадные слои?

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

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

  • Происхождение и ключевое слово !important
  • Строчные стили
  • Слои
  • Специфичность
  • Порядок появления

Происхождение и ключевое слово !important

Происхождение и важность (использование ключевого слова !important) декларации стиля это две разные (но связанные) вещи. Поэтому объясним их обоих ниже.

Стилевые правила могут происходить из одного из следующих источников (в нисходящем порядке приоритетов):

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

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

Давайте возьмем для примера:


html {
  font-size: 16px;
}

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

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

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

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

Кнопка по умолчанию в Инструментах разработчика

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

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

Строчные стили

Если у элемента в атрибуте style задан стиль, то у него будет самая высокая специфичность среди стилей того же уровня без ключевого слова !important.

В следующем примере у элемента <button> будет цвет #fff, потому что у строчных стилей более высокий приоритет.


<button style="color: #fff;">Send</button>


button {
  color: #222;
}

Слои

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


@layer base, custom;

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

@layer custom {
  p {
    font-size: 2rem;
  }
}

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

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

После слоев браузер просматривает правила CSS и, основываясь на специфичности селекторов, решает, кто кого перекрывает.

Вот простой пример. Селектор .button внутри селектора .newsletter имеет более высокую специфичность, чем просто селектор .button. В результате первое правило будет переопределено.


.button {
  padding: 1rem 1.5rem;
}

/* Побеждает этот селектор */
.newsletter .button {
  padding: 0.5rem 1rem;
}

Порядок появления

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

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


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

/* Побеждает этот селектор */
.newsletter .button {
  padding: 0.5rem 1rem;
}

У обоих селекторов в данном примере одинаковая специфичность. И последний побеждает, потому что в коде он расположен после первого.

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

Варианты использования каскадных слоев

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

Переключение тем пользовательского интерфейса

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


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

У нас есть разные слои, и последний из них 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;
    }
  }
}

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


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

@layer themes {
  /* Теперь активна тема custom */
  @layer default, custom;

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

  @layer custom {
    :root {
      --color-primary: #d73a7c;
    }
  }
}

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;
}

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


@layer base, vendors, components;

@layer base {
  /* Базовые стили */
}

/* Импортируем .css файл и назначаем его определенному слою */
@import url(flickity.css) layer(vendors);

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

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

  .dot.is-selected {
    opacity: 1;
  }
}

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

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


<ul class="list">
  <li class="list__item list__item--compact">Пункт 1</li>
  <!-- Другие пункты -->
</ul>

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


/* Этот селектор выигрывает */
.list__item:not(:last-child) {
  margin-bottom: 2rem;
  outline: solid 1px #222;
}

.list__item--compact {
  margin-bottom: 1rem;
}

Селектор .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;
}

Давайте посмотрим, как можно решить эту проблему при помощи каскадных слоев.

Посмотрите на следующий код. Здесь определен слой @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;
    }
  }
}

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

Карточка сообщения в социальной сети

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

Иконка в сообщении ленты имеет размер 24px. В компоненте же комментария ее размер меньше.


@layer feed, comments;

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

@layer comments {
  .comment__icon {
    width: 18px;
    height: 18px;
  }
}

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

Вспомогательный CSS

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

Рассмотрим следующий пример. У нас есть заголовок страницы с вспомогательным классом pх-0. Его задача сбрасывать внутренние отступы в значение 0.


<div class="c-page-header pх-0">
  <!-- Контент -->
</div>

Вот как это можно сделать при помощи каскадных слоев.


@layer base, vendors, components, utils;

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

@layer utils {
  .px-0 {
    padding-left: 0;
    padding-right: 0;
  }
}

Еще детали о каскадных слоях

Стили вне слоев более специфичны

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

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


.button {
  border: 2px solid lightgrey;
}

@layer base, components;

@layer base {
  /* Базовые стили */
}

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

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


@layer base, components;

@layer base {
  /* Базовые стили */
}

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

/* Неявный слой */
@layer {
  .button {
    border: 2px solid lightgrey;
  }
}

Заключение

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