Что такое 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 перебивает то, что пришло по наследству.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.