Явный контроль над специфичностью в CSS?
Мы уже привыкли, что специфичность CSS-селекторов и приоритет соответствующих им правил — одно и то же. Но всегда ли должно быть так? Таким вопросом озадачила на этой неделе рабочую группу CSS легендарная Лия Веру.
В ишью на гитхабе Лия обратила внимание на то, что принцип «более конкретные CSS-правила переопределяют более общие» — всего лишь эвристика. В большинстве простых случаев она помогает, но нередко эта эвристика дает сбой, и тогда приходится прибегать к хакам типа повторения одного класса в селекторе несколько раз или добавления лишних :not(). Лия предлложила вместо этих хаков ввести новый функциональный псевдокласс (например, :filter()
), похожий на :matches()
из селекторов 4 уровня, но c нулевой специфичностью (не меняющий специфичность остальной части селектора).
Идея понравилась Табу Аткинсу, который тут же развил ее: он предложил псевдокласс :is()
с двумя аргументами. Первый аргумент — произвольный селектор (как и у :matches()
), а второй — набор чисел, явным образом задающий желаемую специфичность (например, :is(.foo, 1 2 3)
имел бы специфичность 1 ID, 2 классов и 3 тегов). По умолчанию (без второго аргумента) специфичность равнялась бы нулю. Плюсом этой идеи оказалось то, что ее можно внедрять поэтапно (например, в спецификацию селекторов 4 уровня добавить только вариант с одним аргументом, «обнуляющий» специфичность, а второй аргумент перенести в модуль 5 уровня), чтобы браузерам было проще реализовать ее хоть в каком-то виде.
Другие участники обсуждения предлагали свои варианты. Филип Уолтон (у которого свои давние «счеты» со специфичностью:) предложил «отключать» специфичность на уровне CSS-файла (например, в виде атрибута nospecificity
для <link>
). А Роман Комаров — автор способа переписать практически любой селектор так, чтобы его итоговая специфичность была как у одного класса, с помощью обновленного :not()
из селекторов 4 уровня — предложил вариант создавать свои собственные «слои» («origins» в терминах спецификации CSS Cascade) вдобавок к существующим «авторским», «браузерным», «пользовательским» и т.д. стилям. Хотя это скорее дополнение к селектору :is()
, а не замена ему. Наконец, Франсуа Реми предложил свой вариант названия для нового псевдокласса — :when()
, в чем-то более логичный (например, button:when(:hover)
или input:when([disabled])
).
А вы что думаете? Нужен ли нам такой контроль над специфичностью или это лишь увеличит путаницу вокруг нее? И если нужен, то в каком виде? Подключайтесь к мозговому штурму!
Добавлено 02.12.2017: псевдокласс :is()
только что добавили в черновик спецификации!
Добавлено 02.02.2018: название :is()
забраковали из-за риска путаницы, что из них — противоположность :not()
. Над новым названием пока думают. Присоединяйтесь к обсуждению, ваш аргумент может оказаться решающим!
P.S. Это тоже может быть интересно:
Интересный у Комарова способ…
:not(:not(.bar > *), :not(.foo)), насколько я понял, работает, в связи с тем, что получается логическое «и», между любой прямой потомок класса bar, и классом .foo, получаем
прямой потомок класса bar, являющийся одновременно с этим классом .foo, что и есть:
.bar > .foo, но со специфичностью класса. Ну и так усложнять можно, по-идее, сколько угодно, главное выбрать способ записи такой, чтоб хоть как-то логика прослеживалась, в центре самые «верхние» родительские условия, а дальше слоями добавлять потомков или иные условия. Получается эдакий каскад наоборот :)
Бикус в том, что у :not такая же специфичность, как у самого «специфичного» аргумента, и, если использовать классы, то любая «классная» конструкция будет иметь специфичность класса.
Eсли что-то подобное примут, то тот же .bar > .foo,
можно будет писать, что-то вроде:
:matches(.bar > .foo, as 0, 1, 0) {…}
Тут вот что мне кажется существенным. Идентификатор означает единственность, некая исключительность, которая, по-умолчанию, ведёт к увеличению специфичности. В каких ситуациях может быть актуально, чтоб исключительность не вела к тому, что его условие было приоритетным?
Чисто теоретически, можно допустить ситуацию, когда идентификатор задаёт правила в каком-то определённом состоянии. А в другом состоянии это исключительность перестаёт быть актуальным, например, при наведении… Но это гипотетический пример, в реальной жизни я себе плохо представляю стоит ли оно того…
Матчес я тут немного от фонаря привёл, как один из возможных способов его использования…
Но вообще как-минимум одна ситуация, где мне нужен контроль над специфичностью я сталкивался.
А именно всеми любимая тема отмены отступа между инлайн-блоками(http://css-live.ru/articles/zagadochnye-otstupy-mezhdu-inlajn-blokami.html)
создаёшь класс .no_shift задаёшь ему font-size: 0; а его потомкам .no_shift > * font-size: 1rem. А дальше удивляешься почему у его потомков нужный размер шрифта не проставляется и пользуешься всякими несуразностями вроде .sup.sup . Вот тут бы сбросить специфичность для потомков бы не помешало…
Вот ведь интересный способ — делишься с общественностью, а в голову новые мысли приходят… Я тут вспомнил, можно же специфичность уменьшить через атрибут тега,
div[class=»no_shift»] — если мне память не изменяет — должно работать =)
К сожалению, у селектора по атрибуту специфичность такая же, как у класса (это с ID такой фокус срабатывает). А вообще что касается этого примера, тут, по-моему, проблема не в специфичности, а в наследовании. Может, есть смысл ввести возможность явно наследовать значения не только от непосредственного родителя, а от любого предка — скажем, сделав значение inherit функцией? Типа такого
.no_shift > * { font-size: inherit(2); } /* наследование от предка на 2 уровня выше (т.е. от родителя для самого .noshift) */
и/или такого?
.no_shift > * { font-size: inherit(body); } /* наследование от предка, совпадающего с селектором — в данном случае body */
Но вообще я думаю, что хаки — обоснование скорее для технологии, позволяющей от них избавиться, а не их улучшить. А убирание пробелов в строчном форматировании (где у них есть семантика межсловных разделителей) ради имитации горизонтальной раскладки блоков — это хак. И для него уже есть почти повсеместно доступная замена — флексбоксы. Найти бы пример реального юзкейса, где такой замены (пока) нет…
Илья, согласен с Вами, что улучшать нужно способ, а не приспосабливать неправильный способ и думать что это самый лучший способ :-) Но идея с body всё равно интересная, как-то не рассматривал я раньше этот инхерит, видимо зря :)
Что же касается флексбоксов, то всё-таки это, несколько «лёгкая» технология, которая «на автомате» «делает хорошо», но у неё не настолько обширные возможности выравнивания (а значит, вероятно, и более точного позиционирования элементов) как у гридов, на которые, с надеждой, и взираю…
Да, благодарю за примеры :)
На всякий случай — это чисто гипотетические примеры возможного улучшения стандарта. То значение inherit, которое есть сейчас (в CSS Cascade Level 3/4) — не функция и позволяет наследовать только от непосредственного родителя. Но зато для любого свойства!
Вообще, сама идея класса no_shift в том, чтоб восстанавливать для потомков некое значение по-умолчанию, скажем font-size: 1.6rem. Но нужно делать это так, чтоб класс у потомка имел больший приоритет, и когда мы назначим ему нужный размер шрифта, «общее правило» не перебивало его. Желательно, чтоб вся это конструкция имела некий «запас надёжности», поэтому «в коде ниже» не хочется…
Я пока ничего лучшего чем, «двойное обращение к классу»( .font.font {…} ) не нашёл. Но это ж надо «вручную» отслеживать потомков у которых размер шрифта иной, чем по-умолчанию, что дискредитируют саму идею использования этого класса (no_shift), поскольку возможно проще, в этом случае, вручную назначать размер шрифта и для элемента и для потомков первого уровня.
Ну или флексогридами…