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 — самый сильный селектор». Это ловушка. Правильный ответ — оранжевая.

Браузер выбирает значение свойства не по одному критерию, а по цепочке, и специфичность в ней — только третий шаг:

  1. Происхождение и важность. Объявление с !important из авторского CSS бьёт любое обычное авторское объявление — независимо от специфичности. Наш .danger { color: orange !important; } побеждает ещё до того, как начинается счёт весов.
  2. Специфичность. Если !important нет (или он есть у нескольких правил), сравниваются веса селекторов.
  3. Порядок в исходнике. При равном весе выигрывает то правило, что записано ниже.

Уберите !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 честной итоговой шириной.
Проверьте себя
1. К <button id="save" class="btn danger"> применяются правила: #save { color: blue }, .btn.danger { color: red }, .danger { color: orange !important }. Какой цвет победит?
Ablue — идентификатор имеет наибольшую специфичность
Bred — два класса записаны позже тега
Corange — !important побеждает до сравнения специфичности
Dgreen — победит стиль браузера по умолчанию
2. Текст в flex-элементе не сжимается и растягивает колонку, из-за чего не срабатывает text-overflow: ellipsis. Что чинит проблему?
Amin-width: 0 у flex-элемента
Bflex-wrap: wrap у контейнера
Cposition: relative у контейнера
Dbox-sizing: border-box у flex-элемента