В веб-разработке термин прямые потомки (direct children) обозначает элементы, вложенные в родительский контейнер ровно на один уровень глубже — без промежуточных обёрток. Это ключевое понятие для понимания DOM-дерева и точечного применения стилей через CSS-комбинатор >.
В отличие от всех потомков (descendants), которые включают в себя внуков, правнуков и более глубокие вложенности, прямые потомки — это только «дети» первого уровня. Разница критична при написании селекторов: article p затрагивает любые параграфы внутри статьи, а article > p — лишь те, что лежат непосредственно в <article>.
Как браузер строит дерево DOM и где место прямым потомкам
Когда парсер читает HTML, он создаёт иерархическую модель документа. Каждый тег становится узлом (node), а вложенность определяет родственные связи: родитель (parent), потомок (descendant), ребёнок (child) и прямой потомок (direct child). Последний термин в спецификации DOM Living Standard синонимичен «ребёнку».
Визуально представьте список <ul> с тремя <li>. Если внутри одного пункта вложен ещё один <ul>, то внешний список имеет три прямых потомка — именно эти <li>. Внутренний <ul> уже не является прямым потомком внешнего, он — потомок второго уровня (внук).
⚠️ Внимание: текстовые узлы (пробелы, переносы строк между тегами) тоже считаются дочерними узлами в DOM, но CSS-селектор > работает только с элементами (element nodes), игнорируя текстовые ноды.
CSS-комбинатор >: синтаксис, специфичность и поддержка
Селектор родитель > прямой-потомок выбирает элементы, у которых указанный родитель является непосредственным предком. Специфичность такого селектора складывается из специфичностей обеих частей: например, .menu > li имеет вес 0-1-1 (класс + тег).
Поддержка комбинатора > есть во всех браузерах, начиная с IE7. В современных движках он работает без нареканий, но в очень старых версиях Internet Explorer могли возникать баги с динамически добавленными элементами.
- 🎯 Точечная стилизация — не задевает глубоко вложенные теги
- 🚀 Производительность — браузеру не нужно обходить всё поддерево
- 🧩 Модульность — идеален для БЭМ-блоков и компонентного подхода
- 🔒 Инкапсуляция — защищает внутреннюю структуру от внешних стилей
Практические сценарии: когда нужен строгий контроль вложенности
Классический кейс — навигационное меню. Родительский <ul> должен стилизовать только свои прямые <li>, не затрагивая пункты выпадающих подменю. Селектор .nav > li решает задачу идеально, тогда как .nav li покрасит всё подряд.
Ещё один случай — таблицы. Если нужно выделить только ячейки заголовка <thead>, пишем thead > tr > th. Это гарантирует, что <th> внутри вложенных таблиц (например, в ячейке основной таблицы) не получат случайных стилей.
☑️ Когда использовать прямой комбинатор >
Отличия от смежного селектора + и обобщённого ~
Новички часто путают > с соседним селектором + и родственным ~. Разница фундаментальна: комбинатор > работает по вертикали (родитель → ребёнок), а + и ~ — по горизонтали (брат → брат).
Пример: h2 + p выберет первый параграф сразу после заголовка (если они соседи), а h2 ~ p — все параграфы после заголовка внутри общего родителя. Ни один из них не смотрит вглубь дерева, в отличие от >.
| Селектор | Направление | Что выбирает | Пример |
|---|---|---|---|
parent > child | Вниз (1 уровень) | Прямые дети | ul > li |
parent child | Вниз (любая глубина) | Все потомки | ul li |
prev + next | Вправо (сосед) | Следующий брат | h2 + p |
prev ~ next | Вправо (все братья) | Все следующие братья | h2 ~ p |
Псевдоклассы :first-child, :last-child, :only-child и nth-child
Эти псевдоклассы работают именно в контексте прямых потомков своего родителя. :first-child выбирает элемент, если он является первым ребёнком у своего родителя — независимо от тега. Важно: p:first-child не сработает, если перед <p> стоит <h2>, даже если это первый параграф.
Для выбора по типу используют :first-of-type, :last-of-type, :nth-of-type(n). Они учитывают только элементы указанного тега среди прямых потомков. Это спасает, когда структура смешанная: article > p:first-of-type возьмёт первый параграф, даже если перед ним есть заголовок.
⚠️ Внимание::only-childсработает, только если у родителя всего один дочерний элемент (любого типа). Если рядом есть текстовый узел или комментарий — элемент всё равно считается единственным ребёнком-элементом, но:only-childвернёт false, так как учитывает все узлы.
Нюанс
only-child и текстовые узлы:В спецификации DOM текстовые ноды (пробелы, переносы) являются полноценными дочерними узлами. Поэтому <div><p>Текст</p></div> — у div один ребёнок-элемент, но если HTML отформатирован с переносами, у div будет 3 дочерних узла: текст, p, текст. Селектор p:only-child вернёт false. Решение: используйте :only-of-type или минифицируйте HTML.
JavaScript API: children, firstElementChild, querySelectorAll
В JS свойство parent.children возвращает живую HTMLCollection только из элементов-прямых-потомков (игнорирует текстовые ноды). Это быстрее и удобнее childNodes, который включает всё подряд.
Для поиска по селектору используйте parent.querySelectorAll(':scope > .item'). Псевдокласс :scope завязывает селектор на конкретный элемент, а не на документ. Альтернатива — parent.querySelectorAll('> .item') (работает в современных браузерах).
const list = document.querySelector('.nav');
const directItems = list.children; // HTMLCollection только прямых li
const firstItem = list.firstElementChild; // первый прямой потомок-элемент
const lastItem = list.lastElementChild;
const allDirect = list.querySelectorAll(':scope > li'); // NodeList
В цикле по children лучше конвертировать в массив: [...parent.children].forEach(el => ...). HTMLCollection живая — если вы удаляете элементы внутри цикла, индексы сдвигаются и можно пропустить элементы.
Частые ошибки и паттерны анти-паттерны
Ошибка №1: писать .parent > * для «всех прямых детей». Это работает, но снижает читаемость и может зацепить служебные элементы (например, <script> или <style> внутри компонента). Лучше явно перечислить теги или классы.
Ошибка №2: ожидать, что article > p найдёт параграф, обёрнутый в <div> для стилизации. Верстка часто добавляет обёртки, ломая прямую вложенность. Решение — либо менять селектор на article p, либо использовать :where() для игнорирования обёрток: article > :where(div, section) > p.
⚠️ Внимание: комбинатор > не проходит через Shadow DOM границы. Если компонент использует теневой DOM, стили снаружи не попадут к его прямым потомкам внутри тени. Нужно либо стилизовать изнутри, либо использовать CSS-переменные (custom properties), которые пробивают границу.
Прямой комбинатор > — главный инструмент инкапсуляции стилей в компонентной архитектуре. Он гарантирует, что изменения внутренней структуры потомков не сломают верстку родителя и наоборот.
Продвинутые техники: :where(), :has() и будущие селекторы
С приходом :has() (поддерживается во всех современных браузерах с 2023 года) появилась возможность стилизовать родителя в зависимости от прямых потомков. Например, ul:has(> li.active) подсветит весь список, если у него есть активный прямой потомок.
Функция :where() обнуляет специфичность, что удобно для утилитарных селекторов: :where(article, section) > :where(h1, h2, h3) выберет заголовки первого уровня вложенности с нулевой специфичностью, легко переопределяемой проектными стилями.
В черновике Selectors Level 4 обсуждается комбинатор || для выбора ячеек таблицы по колонкам, но он касается другой оси. Для прямых потомков основной инструмент остаётся >, дополненный псевдоклассами структурных позиций.
Чек-лист: правильная работа с прямыми потомками
- ✅ Используйте
>для изоляции стилей компонентов (БЭМ, CSS Modules) - ✅ Проверяйте реальную вложенность в DevTools (Elements → Computed → наведение на селектор)
- ✅ В JS предпочитайте
childrenиfirstElementChildвместоchildNodes - ✅ Комбинируйте с
:where()для низкой специфичности утилит - ✅ Учитывайте Shadow DOM границы при работе с веб-компонентами
FAQ: частые вопросы про прямых потомков
В чём разница между child и descendant в терминологии CSS?
Child (ребёнок / прямой потомок) — элемент, вложенный ровно на один уровень глубже родителя. Descendant (потомок) — любой элемент внутри родителя на любой глубине: дети, внуки, правнуки и так далее. Селектор parent > child ловит только детей, parent descendant — всех потомков.
Почему p:first-child не работает, если перед p стоит h2?
Псевдокласс :first-child проверяет, является ли элемент первым среди всех детей родителя, а не первым среди параграфов. Если перед <p> есть <h2>, то параграф уже не первый ребёнок. Используйте p:first-of-type для выбора первого параграфа по типу.
Как выбрать все прямые потомки любого типа?
Селектор parent > * выберет все элементы-прямых-потомков. Но будьте осторожны: он зацепит абсолютно всё — включая служебные теги, если они попали в разметку. Часто лучше перечислить нужные теги: parent > div, parent > span, parent > p.
Работает ли > через Shadow DOM границу?
Нет. CSS-селекторы из Light DOM не проникают внутрь Shadow DOM. Прямые потомки внутри теневого дерева невидимы для внешних селекторов с комбинатором >. Стилизуйте их изнутри компонента или используйте CSS-переменные (custom properties), которые наследуются сквозь границу.
Какая специфичность у селектора .menu > li.active?
Складывается из частей: класс .menu (0-1-0), комбинатор не добавляет веса, тег li (0-0-1), класс .active (0-1-0). Итого: 0-2-1. Если нужно снизить вес, оберните классы в :where(): :where(.menu) > :where(li.active) даст 0-0-1.
Понимание разницы между прямыми потомками и всеми потомками — базовый навык для предсказуемой вёрстки. Комбинатор > даёт контроль, производительность и инкапсуляцию, необходимые в современной компонентной разработке.