Исключения – постоянная часть работы со стилями: «все поля, кроме чекбоксов», «все элементы списка, кроме последнего», «все кнопки, кроме основной». Без псевдокласса отрицания такие задачи решаются вторым правилом, перекрывающим первое, или лишними классами в разметке. :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 – в чём разница
| Параметр | Level 3 | Level 4 |
|---|---|---|
| Аргумент | один простой селектор | список через запятую, составные и сложные селекторы |
| Примеры | :not(.foo) | :not(.foo, .bar), h2 :not(span.foo), a:not(table a) |
| Поддержка | все браузеры, включая IE9 | Safari 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 нельзя – селектор станет невалидным, аргументом могут быть только селекторы реальных элементов.
| Синтаксис | Браузеры | Статус |
|---|---|---|
| Один простой селектор (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 и стилями.