Что такое…

Что такое Shadow DOM — и почему твой CSS не достаёт до виджета

?

Иллюстрация: стеклянный купол над маленьким садом посреди большого сада

Ты вставил на сайт готовый виджет чата. Кнопки в нём слишком круглые — ты пишешь в своём CSS button { border-radius: 4px }. Ничего не меняется. Добавляешь !important. Всё равно ничего.

При этом текст в виджете почему-то взял твой шрифт и твой цвет. А document.querySelector(".send") возвращает null, хотя кнопку «Отправить» ты прямо сейчас видишь на экране.

Это не три разных бага. Это одна штука — Shadow DOM.

Что такое Shadow DOM простыми словами

Обычная страница — это одно большое дерево элементов, DOM. Любой твой CSS-селектор и любой querySelector ищут по всему этому дереву.

Shadow DOM — это отдельное маленькое дерево, прикреплённое к одному элементу. Как стеклянный купол над клумбой посреди сада. Клумбу видно, она часть сада. Но садовник с секатором снаружи до неё не дотянется.

Элемент, к которому прикреплён купол, называют хостом. Само скрытое дерево — теневым корнем (shadow root). В DevTools ты увидишь его как строку #shadow-root (open) внутри элемента.

Зачем это нужно? Чтобы виджет работал одинаково на любом сайте. Автор чата не знает, какие стили у тебя. Если бы твой button { } ломал его кнопки, виджет пришлось бы чинить под каждого клиента. Купол защищает в обе стороны: и его стили не вылезают наружу и не портят твою страницу.

Что проходит сквозь купол, а что нет

Вот тут и прячется загадка со шрифтом.

Не проходят — селекторы. Правило button { } со страницы просто не видит кнопки под куполом. Специфичность и !important тут ни при чём: селектор не находит элемент вообще. Это частая причина, когда CSS не работает, а инспектор показывает, что правило даже не применилось.

Проходят — наследуемые свойства. color, font-family, font-size, line-height наследуются от родителя к ребёнку. А хост — родитель всего, что под куполом. Поэтому шрифт и цвет твоей страницы протекают внутрь, а border-radius — нет: он не наследуется.

Проходят — CSS-переменные. Они тоже наследуются. И это официальный способ настраивать виджет снаружи. Хороший автор пишет внутри background: var(--chat-accent, blue), а ты у себя ставишь --chat-accent: #d4a373. Про них подробно — в статье про CSS-переменные.

Проходят — помеченные части. Автор может пометить элемент атрибутом part="send". Тогда снаружи его можно стилизовать так: chat-widget::part(send) { border-radius: 4px }. Только те части, которые разрешили.

Почему querySelector не находит кнопку

document.querySelector тоже ищет только в основном дереве. Под купол он не заглядывает.

Если купол открытый (open), можно войти явно:

const widget = document.querySelector("chat-widget");
const send = widget.shadowRoot.querySelector(".send");

Если закрытый (closed), widget.shadowRoot вернёт null. Автор специально запретил лезть внутрь — и обходить это не стоит: при обновлении виджета твой код сломается.

С событиями похожая история. Клик по кнопке внутри всплывёт наружу, но event.target у тебя покажет хост, а не кнопку. Купол подменяет цель, чтобы не выдавать внутренности. Настоящий путь клика вернёт event.composedPath() — для открытого купола.

Где ты уже встречал Shadow DOM

Каждый день, просто не знал.

  • Стандартные элементы браузера. Кнопки плеера у <video controls>, ползунок <input type="range">, календарик у поля даты — всё это теневые деревья самого браузера. Вот почему ползунок так трудно перекрасить: туда пускают только через специальные псевдоэлементы вроде ::-webkit-slider-thumb.
  • Веб-компоненты. Свои теги вроде <chat-widget> или <model-viewer> почти всегда живут под куполом.
  • Встраиваемые штуки. Виджеты оплаты, поддержки, отзывов. Купол — ровно то, что не даёт им сломаться на чужом сайте.

Чтобы увидеть купола браузера в DevTools, включи в настройках «Show user agent shadow DOM».

Как попросить ИИ поправить виджет

Нейросеть по привычке предложит !important и усиленные селекторы. Объясни ей механизм:

Слабый промптстили не применяются к кнопкам виджета, почини
Сильный промптВиджет chat-widget рендерит кнопки в Shadow DOM, поэтому мой CSS-селектор button до них не доходит. Не предлагай !important и усиление селекторов. Проверь, какие CSS-переменные и какие part-атрибуты виджет открывает наружу, и настрой скругление кнопок через них. Если ни того ни другого нет — так и скажи.

Shadow DOM и Virtual DOM — это одно и то же?

Нет. Virtual DOM — способ React быстро обновлять страницу, он живёт в памяти. Shadow DOM — настоящая изоляция кусочка страницы в браузере. Общее у них только слово DOM.

Можно ли создать Shadow DOM без JavaScript?

Да. Тег <template shadowrootmode="open"> внутри элемента создаёт купол прямо из HTML. Все основные браузеры поддерживают это с 2024 года, в том числе для серверного рендеринга.

Как сбросить унаследованный шрифт внутри своего компонента?

Внутри купола задай стили для хоста: :host { font-family: system-ui; color: inherit } или конкретные значения. Правило для :host перебивает то, что пришло по наследству.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →