Псевдокласс :not()

:not() – CSS-псевдокласс отрицания: выбирает элементы, не соответствующие селектору-аргументу. В Selectors Level 4 принимает список селекторов (:not(.a, .b)) – кроссбраузерно с января 2021 (Chrome 88, Firefox 78, Safari 9).

3 минуты чтения

Исключения – постоянная часть работы со стилями: «все поля, кроме чекбоксов», «все элементы списка, кроме последнего», «все кнопки, кроме основной». Без псевдокласса отрицания такие задачи решаются вторым правилом, перекрывающим первое, или лишними классами в разметке. :not() переносит исключение прямо в селектор. Псевдокласс появился ещё в Selectors Level 3 в урезанном виде, а в Selectors Level 4 стал полноценным инструментом со списком селекторов в аргументе. Вместе с реляционным :has() он образует логику современных селекторов: :has() проверяет наличие, :not() – отсутствие.

Как это работает

Синтаксис – селектор:not(аргумент): правило применяется к элементам, которые не соответствуют ни одному селектору из аргумента. p:not(.fancy) – «параграфы без класса fancy», input:not([type="checkbox"]) – «все поля ввода, кроме чекбоксов». Возможности аргумента зависят от поколения спецификации.

:not() в Selectors Level 3 и Level 4 – в чём разница

Два поколения синтаксиса :not()
ПараметрLevel 3Level 4
Аргументодин простой селекторсписок через запятую, составные и сложные селекторы
Примеры:not(.foo):not(.foo, .bar), h2 :not(span.foo), a:not(table a)
Поддержкавсе браузеры, включая IE9Safari 9 (2015), Firefox 78 (2020), Chrome и Edge 88 (январь 2021)
Обход для старых браузеровне нуженцепочка :not(.foo):not(.bar)

Отсюда типовой вопрос: чем :not(a):not(b) отличается от :not(a, b)? По результату – ничем, оба означают «ни a, ни b». Цепочка – И-логика старого синтаксиса, работающая везде уже больше десяти лет; список – компактная запись Level 4. Ошибка появляется, когда список читают по аналогии с :is(): :not(.foo, .bar) – это «не (.foo ИЛИ .bar)», то есть исключаются оба класса сразу.

Второй важный нюанс: список аргументов :not() – unforgiving («непрощающий»), один невалидный или неподдерживаемый браузером селектор инвалидирует правило целиком. Это прямой контраст с forgiving-списками :is(), :where() и :has(). Приём для устойчивости – обернуть аргументы в :is():

/* База: все параграфы, кроме .fancy */
p:not(.fancy) { color: green; }

/* Level 4: ни .foo, ни .bar */
div:not(.foo, .bar) { color: red; }

/* То же самое в Level 3-синтаксисе */
div:not(.foo):not(.bar) { color: red; }

/* Отступы между элементами списка, кроме последнего */
li:not(:last-child) { margin-block-end: 8px; }

/* Unforgiving-список, защищённый через :is() */
p:not(:is(.foo, :unsupported-thing)) { color: green; }

Специфичность :not() устроена так же, как у :has(): сам псевдокласс ничего не добавляет, считается самый специфичный селектор из аргумента. #foo:not(#bar) выбирает те же элементы, что и просто #foo, но весит как два ID – перебить такое правило почти невозможно. Подробнее о подсчёте – в статье о специфичности CSS.

Зачем это нужно

Главный выигрыш – меньше служебных классов и правил-перекрытий. В дизайн-системах :not() задаёт общие стили «всем, кроме» отдельных компонентов без цепочек переопределений; в шаблонах 1С-Битрикс, где типовые компоненты приходят со своей разметкой, он исключает служебные элементы из общих правил без копирования и правки шаблона. Но у псевдокласса есть ловушки, которые регулярно ловят даже опытных верстальщиков:

  • Матчит html и body. Одиночный :not(.foo) применится ко всем элементам без этого класса, включая html и body, – охват шире ожидаемого. Уточняйте селектор слева: p:not(.foo).
  • Не исключает ветку DOM. body :not(table) a всё равно покрасит ссылки внутри таблицы: между table и ссылкой стоят tbody, tr и td, а каждый из них сам матчит :not(table). В Level 4 задача решается сложным селектором в аргументе: a:not(table a).
  • :not(*) не выбирает ничего. Элемент, который «не элемент», невозможен.
  • Псевдоэлементы запрещены. Передать внутрь ::before или ::first-line нельзя – селектор станет невалидным, аргументом могут быть только селекторы реальных элементов.
Поддержка :not() браузерами
СинтаксисБраузерыСтатус
Один простой селектор (Level 3)все, включая IE9+Baseline Widely available с июля 2015
Список селекторов (Level 4)Safari 9+, Firefox 78+, Chrome и Edge 88+кроссбраузерно с января 2021

Пример

Типовая задача вёрстки – отступы между элементами списка: правило li:not(:last-child) из блока кода выше заменяет пару «всем задать отступ, последнему обнулить» одним селектором. Тот же подход работает с порядковыми псевдоклассами вроде :nth-child(): tr:not(:nth-child(-n + 2)) исключит из правила первые две строки таблицы.

Вторая показательная связка – «отрицание по потомку» вместе с :has():

/* Figure без подписи – убираем нижний отступ */
figure:not(:has(figcaption)) { padding-block-end: 0; }

/* Карточки без изображения получают рамку */
.card:not(:has(img)) { border: 1px solid #e0e0e0; }

До Selectors Level 4 выбрать «элементы, внутри которых чего-то нет» средствами CSS было невозможно – требовался JavaScript, который проверял содержимое и навешивал классы. Сейчас достаточно одного правила, а разметка остаётся чистой: ни классов-модификаторов, ни скриптов, ни рассинхронизации между DOM и стилями.

Частые вопросы

Можно ли передать в :not() несколько селекторов?

Да, начиная с Selectors Level 4 аргументом может быть список через запятую: button:not(.primary, .danger). Поддержка: Safari 9+, Firefox 78+, Chrome и Edge 88+ (январь 2021). Для максимальной совместимости тот же результат даёт цепочка button:not(.primary):not(.danger).

Чем :not(a):not(b) отличается от :not(a, b)?

По результату они эквивалентны: оба означают «ни a, ни b». Разница в поколении синтаксиса и совместимости: цепочка – Selectors Level 3, работает во всех браузерах, включая устаревшие; список – Level 4, кроссбраузерно с января 2021. Важно не путать список с :is(): :not(a, b) исключает оба варианта сразу.

Какая специфичность у :not()?

Сам псевдокласс специфичности не добавляет – считается самый специфичный селектор из аргумента. #foo:not(#bar) матчит те же элементы, что и #foo, но имеет специфичность двух ID. Поэтому ID внутрь :not() без необходимости лучше не передавать.

Почему правило с :not() перестаёт работать целиком?

Скорее всего, в аргументе есть невалидный или неподдерживаемый селектор: список :not() – unforgiving, одна ошибка инвалидирует всё правило. Лечится обёрткой в forgiving-список :is(): p:not(:is(.foo, :experimental)) – невалидная часть просто игнорируется.

Матчит ли :not(.foo) теги html и body?

Да. Без уточняющего селектора слева :not(.foo) применится ко всем элементам без класса foo, включая html и body, – охват получается шире ожидаемого. Указывайте элемент или контекст: p:not(.foo) вместо одиночного :not(.foo).

Почему body :not(table) a красит ссылки внутри таблицы?

Потому что между table и ссылкой стоят tbody, tr и td, а каждый из них сам матчит :not(table). :not() исключает только сам элемент, а не ветку DOM. В Selectors Level 4 задача решается сложным селектором в аргументе: a:not(table a).

Можно ли использовать псевдоэлементы внутри :not()?

Нет, ::before, ::after и другие псевдоэлементы внутри :not() запрещены спецификацией – селектор станет невалидным. :not() принимает только селекторы реальных элементов: теги, классы, атрибуты, псевдоклассы.

Материалы по теме

Валентина Меланина

Нужна консультация?

Разберу ваш сайт и покажу точки роста

Если хотите понять, как этот термин применить к вашему проекту — начнём с аудита.