Допустим, мы разрабатываем библиотеку компонентов.

Допустим, мы используем React.

Допустим, в ней есть компонент кнопки.

Очень условно он будет выглядеть так:

// Button.js

import './Button.css'

const Button = ({ children, className = '' }) => {
	const cls = 'Button' + className;
	return <button className={cls}>{children}</button>
}

А стили будут выглядеть так:

/* Button.css */

.Button {
	background: blue;
	padding: 4px 10px;
	border: none;
}

Тут к нам приходят разрабы и говорят: "кнопка классная, но нам нужна ссылка в виде кнопки"

Не вопрос! Добавим свойство Component:

// Button.js

import './Button.css'

const Button = ({ children, Component = 'button', className = '' }) => {
	const cls = 'Button' + className;
	return <Component className={cls}>{children}</Component>
}

Пример использования:

const linkButton = (
	<Button Component="a" href="https://vk.com">
		ВКонакте
	</Button>
);

Только ой, a ж является инлайновым элементом. Что такого, спросите вы? Кнопка вроде как выглядела, так и выглядит. А вот нет. Представьте, что пользователи в месте встраивания кнопки хотят добавить ей отступ справа:

// App.js

<Button className="myButton">Click it!</Button>
/* App.css */

.myButton {
	margin-right: 10px;
}

Инлайновый элемент проигнорирует этот margin.

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

/* Button.css */

.Button {
	background: blue;
	padding: 4px 10px;
	border: none;
	display: inline-block; /* Зафиксируем display */
}

Но теперь возникла другая беда, куда более подлая и трудноизлечимая. Следите за руками. Представьте себе такой способ использования кнопки в проекте. Допустим, там есть какой-то стейт loading, который если true, то кнопку нужно скрыть:

// App.js

<Button className={loading ? 'hidden' : ''} />
/* App.css */

.hidden {
	display: none;
}

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

Беда заключается в том, что у .hidden и .Button одинаковый вес, при этом оба они претендуют на свойство display. А значит CSS-парсеру придется выяснять победителя исходя из порядка. Порядок — это последний уровень каскада. Когда ваши CSS-файлы разбиты по модулям, рассчитывать на их определенный порядок появления в итоговом бандле очень некомфортно. В итоге это будет приводить к ситуациям, когда побеждает то один селектор, то другой. Что же делоть...

Layer спешит на помощь

Новый уровень каскада Layer разместился аккурат перед специфичностью и порядком:

  1. Importance

  2. Context

  3. Layer (приветик)

  4. Specificity

  5. Order

Layer позволяет гибко задать разные уровни собственных стилей. В нашем примере таких уровней может быть три (а может быть и больше):

  1. уровень сброса стилей;

  2. уровень стилей библиотеки;

  3. уровень стилей приложения.

Давайте эти уровни зададим.

@layer reset, library; /* Деклалируем приоритет слоёв */

/* Button.css */

@layer reset {
	.Button {
		display: inline-block;
	}
}

@layer library {
	.Button {
		background: blue;
		padding: 4px 10px;
		border: none;
	}
}

Но постойте, мы вроде хотели три уровня задать, а задали два. Это специфика каскада layer. Всё, что определено за пределами какого-либо уровня, автоматически становится самым приоритетным. На самом деле и library тут не нужен, но я его оставил для наглядности.

Прикольно, что все стили вне уровней побеждают, так как пользователям не нужно свои перебивающие стили обрамлять в какой-нибудь @layer app.

То есть вот этот стиль в итоге перебьёт тот, что находится в @layer reset.

/* App.css */

.hidden {
	display: none; /* Наш победитель */
}

Удобно? По моему это просто фантастика. Во многом потому, что в браузерах этого каскада ещё нет :( пока.

Комментарии (13)


  1. efkz
    17.02.2022 10:08
    +6

    Тут к нам приходят разрабы и говорят: "кнопка классная, но нам нужна ссылка в виде кнопки"

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

    В целом, складывается ощущение, что слои - очередная вещь, которая призвана упростить жизнь, но на самом деле всё усложнит.


    1. ArthurSupertramp Автор
      17.02.2022 10:17
      +1

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


    1. ArthurSupertramp Автор
      17.02.2022 10:26
      +1

      Ну и я могу изи привести пример, в котором нет нарушения закона, о котором ты говоришь, но есть описанная проблема. Представь, что ты делаешь компонент заголовка и сбрасываешь ему браузерный margin до нуля. Его так же сложно будет перебить потом пользователям либы.


      1. efkz
        17.02.2022 13:29

        Щас вопросики полезут как червячки (с)

        1. Зачем сбрасывать браузерный маргин, чтобы потом его заново устанавливать?
        2. Почему бы не использовать природную специфичность стилей:
        h1{margin:0}
        h1.myfancystyle{margin: 1em 0 2em}
        #myveryownmodal h1.myfancystyle{margin: 0 0 2em}
        3. Почему бы не использовать природный усилитель !important ?
        4. Почему бы не использовать style="margin:25vw", если уж припёрло?


        1. ArthurSupertramp Автор
          17.02.2022 13:37
          +1

          1. Затем, что допустим большинству нужен margin, сброшенный до какого-то значения, а части пользователей требуется его переопределить.

          2. Твой h1 будет аффектить ваще все `<h1 />` на странице. На больших проектах с несколькими независимыми фронтендами такой ресет приводит к проблемам.

          3. Использую !important в самых патовых ситуациях и не считаю, что он подходит для решения проблемы, которую я описал.

          4. Скажу то же самое, что сказал пунктом ранее.


    1. BerkutEagle
      17.02.2022 12:58
      +1

      Кнопка для перехода на другую страницу сайта. Как сделать "Открыть в новой вкладке" в контекстном меню без использования ссылки?


      1. arsenty
        18.02.2022 18:44

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


  1. wadowad
    17.02.2022 11:28

    Если мы скрываем именно кнопку, то почему бы не использовать
    более специфичное сочетание .Button.hidden {} ?


    1. ArthurSupertramp Автор
      17.02.2022 11:33
      +1

      Это норм решение, но мне кажется, что в долгосрочной перспективе такое повышение специфичности — дорога в ад. С каждым следующим витком будет сложнее вносить правки в стили.


      1. efkz
        17.02.2022 13:30
        +1

        слои это такое же повышение специфичности, только другими методами


        1. ArthurSupertramp Автор
          17.02.2022 13:33
          +1

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

          Не претендую на правоту, но способ, который предлагает Layer кажется гораздо более простым, чем определение приоритета на уровне специфичности.


  1. essome
    17.02.2022 15:34

    Эта проблема решена в Angular.

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

    @Component({selector: '[app-button]', template: `<ng-content></ng-content>`})
    export class ButtonComponent {}

    а потом навешиваем его куда хотим, на кнопку или ссылку

    <a href="/" app-button>Link Text</a>
    <button  app-button>Button Text</button>

    Для стилей в angular есть :host который привязывается напрямую к компоненту, ему не важно какой тег у него тег

    // будет как работать как для a, так и для button
    
    :host {
      display:inline-flex;
    }
    
    


    1. ArthurSupertramp Автор
      18.02.2022 07:32

      Никогда не писал на Angular. Выглядит угарно! А во что в итоге этот код превращается? В первую очередь интересует CSS.