CSS: специфичность, flex и позиционирование
«Вот разметка, вот CSS — какого цвета будет кнопка?» За тридцать секунд этот вопрос проверяет, понимаете ли вы каскад, специфичность и box-модель — или просто подбираете свойства методом тыка.
Специфичность — числовой вес селектора, по которому браузер выбирает победителя, когда одно и то же свойство задано несколькими правилами.
Вопрос-крючок
Интервьюер показывает вам два блока и молча ждёт.
<button id="save" class="btn danger">Удалить</button>
#save { color: blue; }
.btn.danger { color: red; }
button { color: green; }
.danger { color: orange !important; }
Типичный ответ: «Синяя, ведь id — самый сильный селектор». Это ловушка. Правильный ответ — оранжевая.
Браузер выбирает значение свойства не по одному критерию, а по цепочке, и специфичность в ней — только третий шаг:
- Происхождение и важность. Объявление с
!importantиз авторского CSS бьёт любое обычное авторское объявление — независимо от специфичности. Наш.danger { color: orange !important; }побеждает ещё до того, как начинается счёт весов. - Специфичность. Если
!importantнет (или он есть у нескольких правил), сравниваются веса селекторов. - Порядок в исходнике. При равном весе выигрывает то правило, что записано ниже.
Уберите !important — и кнопка станет синей: #save весит больше, чем .btn.danger. Именно этот вопрос-перевёртыш и хотят услышать: кандидат, который знает только «id сильнее класса», отвечает уверенно и неверно.
Как считается специфичность
Вес — это тройка чисел (A, B, C), которую сравнивают слева направо, как версии программы.
| Разряд | Что считаем | Пример |
| A | идентификаторы | #save → 1-0-0 |
| B | классы, атрибуты, псевдоклассы | .btn[disabled]:hover → 0-3-0 |
| C | теги и псевдоэлементы | li::marker → 0-0-2 |
Посчитаем несколько селекторов кодом — заодно видно, что комбинаторы (>, пробел, +) веса не добавляют вообще.
function specificity(sel) {
const ids = (sel.match(/#[a-z0-9_-]+/gi) || []).length;
const classes = (sel.match(/\.[a-z0-9_-]+/gi) || []).length;
const attrs = (sel.match(/\[[^\]]*\]/g) || []).length;
const pseudo = (sel.match(/:(hover|focus|not|checked|first-child)/gi) || []).length;
const tags = (sel.match(/(^|[\s>+~])[a-z]+/gi) || []).length;
return [ids, classes + attrs + pseudo, tags].join('-');
}
const list = ['button', '.btn.danger', '#save', 'header nav a:hover', '.btn[disabled]'];
for (const sel of list) {
console.log(sel + ' → ' + specificity(sel));
}
Результат:
button → 0-0-1
.btn.danger → 0-2-0
#save → 1-0-0
header nav a:hover → 0-1-3
.btn[disabled] → 0-2-0
Что спрашивают вдогонку
- Инлайн-стиль в атрибуте
styleсильнее любого селектора из таблицы стилей, но слабее!important. - Разряды не «переполняются». Одиннадцать классов дают 0-11-0 — и всё равно проигрывают одному
id(1-0-0). Десятичной арифметики здесь нет. :where(...)всегда весит 0, а:is(...)и:not(...)берут вес самого тяжёлого аргумента внутри. Поэтому:where(.a, #b) pвесит 0-0-1 — им удобно писать стили, которые легко перебить.- Универсальный селектор
*веса не имеет.
Flex или grid
Формулировка «одна ось против двух» верна, но звучит как заученная. Добавьте вторую половину ответа: во flex размеры диктует контент, в grid — контейнер.
/* Flex: ряд карточек, ширина подстраивается под содержимое */
.toolbar {
display: flex;
align-items: center;
gap: 16px;
}
.toolbar__spacer { flex: 1; } /* «резиновая» распорка */
/* Grid: каркас страницы, ячейки заданы заранее */
.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
}
.layout__sidebar { grid-area: sidebar; }
Практическое правило: раскладка страницы и таблицеподобные сетки — grid; ряд кнопок, чипсы, шапка карточки — flex. Они не конкуренты: grid снаружи, flex внутри ячеек — самая частая связка в бою.
Ловушка, которую любят на собеседовании
«Почему длинный текст в flex-колонке растянул её и сломал text-overflow: ellipsis?» Потому что у flex-элемента min-width по умолчанию равен auto: он не может стать уже своего содержимого.
.row { display: flex; gap: 16px; }
.row .cell { flex: 1; min-width: 0; } /* ← вот эта строка чинит всё */
.row .title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
Знание про min-width: 0 мгновенно отличает человека, который верстал реальные интерфейсы, от того, кто читал про flex в статье.
Почему position: absolute «сломал» вёрстку
Абсолютно позиционированный элемент считает top/right/bottom/left не от родителя, а от содержащего блока — ближайшего предка, у которого position отличается от static. Если такого предка нет, отсчёт идёт от начального содержащего блока, то есть фактически от окна: бейдж уезжает в угол экрана.
.card { position: relative; } /* без этой строки бейдж улетит в угол окна */
.card__badge {
position: absolute;
top: 8px;
right: 8px;
}
Второй эффект: абсолютный элемент выпадает из потока. Родитель перестаёт учитывать его высоту и может схлопнуться в ноль — «вёрстка поехала» ровно отсюда. И третье, что мало кто называет: transform, filter или will-change на предке тоже создают содержащий блок — даже для position: fixed. Именно поэтому «фиксированное» модальное окно внутри анимированной карточки вдруг перестаёт быть фиксированным.
box-sizing: 300px, которые не 300px
.input { width: 300px; padding: 12px; border: 1px solid #ccc; }
/* content-box (значение по умолчанию):
реальная ширина = 300 + 12*2 + 1*2 = 326px */
*, *::before, *::after { box-sizing: border-box; }
/* теперь 300px — это ВСЯ ширина: padding и border живут внутри */
Без border-box два блока по width: 50% с паддингами не влезают в строку — классический баг «две колонки по 50% переносятся». Поэтому глобальный сброс box-sizing стоит в начале почти любого проекта.
Как это работает под капотом
Получив HTML и CSS, браузер строит DOM и CSSOM, затем для каждого элемента считает итоговые стили: собирает все подходящие объявления и сортирует их — сначала по происхождению и важности (стили браузера, пользователя, автора, !important-версии в обратном порядке), затем по слоям @layer, затем по специфичности, и только в самом конце — по порядку в файле. Побеждает одно значение, остальные отбрасываются.
Дальше — layout (браузер вычисляет геометрию: тут и работают box-sizing, flex, содержащие блоки), paint (заливка пикселей) и composite (склейка слоёв). Изменение width или top запускает пересчёт геометрии всей ветки — reflow. Изменение transform и opacity обычно обходится композитингом, поэтому анимации делают именно на них: это самый частый follow-up вопрос после разговора о позиционировании.
Частые ошибки на собеседовании
- «id всегда побеждает». Не побеждает —
!importantи инлайн-стиль сильнее. - Складывать специфичность в уме как число. 0-11-0 не превращается в 1-1-0.
- Считать комбинаторы.
>и+веса не добавляют. - Обещать «победить специфичность через !important». В проде это тупик: следующий
!importantперебьёт ваш. Правильный ответ — снизить вес селекторов, использовать@layerили:where(). - Говорить «flex для всего». Интервьюер ждёт границу применимости, а не преданность одному свойству.
- Забыть про
position: relativeу родителя и не суметь объяснить, что такое содержащий блок.
Итоги-шпаргалка
- Порядок выбора: важность → специфичность → порядок в коде. Специфичность — не первый критерий.
- Вес считается как
(id, классы+атрибуты+псевдоклассы, теги), разряды не переполняются. :where()= 0,:is()/:not()= вес самого тяжёлого аргумента.- Flex — одна ось, размер от контента; grid — две оси, размер от контейнера. В бою grid снаружи, flex внутри.
min-width: 0— лекарство от flex-элемента, который не хочет сжиматься.position: absoluteсчитает координаты от ближайшего непозиционированного предка и выпадает из потока;transformна предке создаёт содержащий блок даже дляfixed.box-sizing: border-boxделаетwidthчестной итоговой шириной.