Тег slot (от английского slot, «место», «гнездо») задаёт место в готовом компоненте, куда браузер подставляет ваш текст или другие элементы. Например, рамку и оформление карточки делает сам компонент, а содержимое карточки записываете вы. Разметка компонента хранится в теневом DOM, поэтому слот записывают именно там.
Если для слота нет подходящего содержимого, браузер показывает запасное содержимое, записанное внутри самого тега slot. Вне теневого DOM слот ничего не делает: страница просто показывает его запасное содержимое.
Слот обычно используют вместе с шаблоном теневого DOM. О шаблонах читайте в статье про тег template. В статье разобраны правила распределения содержимого, запасное содержимое, ловушка с пробелами, стилизация через ::slotted, методы JavaScript для работы со слотами, событие slotchange, вложенные слоты и ручное распределение. Другие теги группы собраны в шпаргалке по тегам.
<user-card>
<template shadowrootmode="open">
<h3><slot name="title">Без названия</slot></h3>
<slot>Описание не добавлено</slot>
</template>
<span slot="title">Анна</span><p>Дизайнер</p>
</user-card>
class UserCard extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<h3><slot name="title">Без названия</slot></h3>
<slot>Описание не добавлено</slot>`;
}
}
customElements.define("user-card", UserCard);
В этой статье
- Тег slot в HTML: что это и где он работает
- Атрибуты тега slot: name и глобальный slot
- Как содержимое попадает в слот: правила распределения
- Запасное содержимое слота и ловушка с пробелами
- Стили слота: ::slotted, display: contents и наследование
- JavaScript для слотов: assignedNodes и assignedSlot
- Событие slotchange: когда слот узнаёт об изменениях
- Вложенные слоты и передача содержимого вниз
- Ручное назначение: slotAssignment и assign()
- Почему тег slot не работает
Тег slot в HTML: что это и где он работает
Сначала о том, какие части нужны слоту и что он делает без них.
Слоту нужны два участника: компонент и элементы, которые в него вставляют. Компонентом здесь называют обычный элемент страницы со спрятанной внутренней разметкой, она называется теневым DOM. Вставляемые элементы: обычные дочерние элементы компонента, записанные между его тегами. Теневой DOM создаёт скрипт методом attachShadow() или атрибут shadowrootmode у template прямо в разметке. Браузер выводит внутреннюю разметку вместо дочерних элементов компонента, а слоты показывают, в каком месте внутренней разметки окажутся эти дети. Сами дети остаются в документе: их находит document.querySelector, и на них действуют стили страницы.
Слот без теневого DOM ничего не распределяет. Если записать <slot> в обычную разметку, у него будет display: contents, нулевые размеры и пустой assignedNodes(), а на странице выведется его запасное содержимое. Элемент со slot внутри компонента без теневого DOM виден как обычный: атрибут ничего не меняет.
Тег допустим там, где допустим текст: внутри p валидатор его принимает, а внутри ul отмечает ошибкой. Закрывающий </slot> обязателен.
Атрибуты тега slot: name и глобальный slot
У slot один собственный атрибут, а вставляемые элементы получают парный глобальный атрибут slot.
| Атрибут | Где записывают | Что делает | Blink / Firefox / Safari |
|---|---|---|---|
name | На теге slot внутри теневого DOM | Задаёт название слота. Слот без name или с name="" безымянный и берёт всё, что не нашло другого слота | 53 / 63 / 10 |
slot | На вставляемом элементе, прямом ребёнке компонента | Выбирает слот с таким же name. Пустое значение или отсутствие атрибута отправляет элемент в безымянный слот | 53 / 63 / 10 |
slot на теге slot | На слоте-посреднике внутри другого компонента | Назначает сам слот слоту внутреннего компонента | 53 / 63 / 10 |
В значении name допустимы пробелы и пустая строка: валидатор ошибки не даёт. Слот не имеет собственной роли для программ чтения экрана, поэтому смысл задают вставляемые элементы.
Как содержимое попадает в слот: правила распределения
Распределением слотов управляют несколько простых правил.
Элемент со slot="title" попадает в слот <slot name="title">. Элементы без атрибута и текст между ними попадают в безымянный слот. Распределяются только прямые дети компонента: вложенный элемент, даже со slot, в слот сам не попадает, а выводится вместе со своим родителем.
Элемент, для которого в теневом DOM нет слота с таким названием, не выводится, хотя остаётся на странице и находится через document.querySelector. То же происходит со всем остальным, если нет безымянного слота. Текст без тега можно направить только в безымянный слот.
Пример: что попадает в слот
Добавляйте элементы по одному. Слот title получает только прямых детей с slot=title, безымянный слот получает остальных и текст. Элемент с чужим названием слота остаётся на странице, но не выводится, а i внутри span в слот не попадает.
Если в слот попали несколько элементов, они выводятся в порядке документа. Если в теневом DOM два слота с одним name, все узлы получает первый, а второй остаётся пустым, пока первый на месте.
Пример: повторное название слота и порядок
Все три элемента попадают в первый слот с этим названием, второй остаётся пустым. Если первый слот убрать, элементы переходят во второй. Порядок внутри слота совпадает с порядком элементов в разметке.
Запасное содержимое слота и ловушка с пробелами
Запасное содержимое показывается, пока слот ничего не получил.
Всё, что записано внутри <slot>, служит запасным: текст, элементы, даже другой слот. Как только в слот попал хотя бы один узел, запасное содержимое пропадает, а когда узел убрали, возвращается. Метод assignedNodes() запасное содержимое в список не включает.
Ловушка связана с пробелами. Перенос строки и отступ между тегами компонента браузер считает текстовыми узлами и отдаёт безымянному слоту. Поэтому в такой записи запасное содержимое безымянного слота не появится: слот получил пустой текст.
<user-card>
<span slot="title">Анна</span>
</user-card>
Пустой компонент и комментарий внутри него запасное содержимое не скрывают, а один пробел, перенос строки или любой текст скрывают. Чтобы запасное содержимое показывалось, теги внутри компонента записывают вплотную друг к другу.
Пример: запасное содержимое и пробелы
Пустой компонент и комментарий оставляют запасное содержимое. Один пробел или перенос строки между тегами браузер считает назначенным текстом, и запасное содержимое безымянного слота пропадает, хотя на экране ничего нет.
Стили слота: ::slotted, display: contents и наследование
Стили слота работают не так, как у обычных элементов.
У slot по умолчанию display: contents: собственного блока нет, поэтому фон, рамка, отступы и размеры на нём не рисуются. Если задать display: block, рамка появится.
Вставляемые элементы остаются элементами страницы, и стили страницы действуют на них как обычно. Изнутри компонента на них влияет ::slotted(селектор), но только на сам назначенный элемент, потомки не затрагиваются: ::slotted(b) не покрасит b внутри абзаца. Если стиль страницы и ::slotted задают одно свойство, побеждает стиль страницы (без !important; при !important порядок обратный). Псевдоэлемент ::slotted работает с Blink 50, Firefox 63 и Safari 10.
::slotted(p) {
color: #C0392B;
}
slot {
display: block;
}
Наследование идёт по итоговой структуре, где слот заменён своим содержимым. Поэтому color и font-style обёртки внутри компонента доходят до вставляемых элементов.
Пример: стили слота и ::slotted
Выберите правило и посмотрите на цвета и рамку. Стиль на самом slot не рисуется, пока у него display: contents, а стиль страницы для элемента побеждает ::slotted. Цвет, заданный обёртке внутри компонента, доходит до вставляемых элементов по наследованию.
JavaScript для слотов: assignedNodes и assignedSlot
Скрипт читает распределение через методы слота и свойство элемента.
Метод slot.assignedNodes() возвращает узлы, назначенные слоту, включая текст, а slot.assignedElements() возвращает только элементы. С параметром { flatten: true } пустой слот возвращает запасное содержимое, а слоты-посредники заменяются итоговыми узлами. Свойство element.assignedSlot у вставленного узла возвращает его слот, а у узла без слота возвращает null. Методы assignedNodes() и assignedSlot работают с Blink 53, Firefox 63 и Safari 10, а assignedElements() работает с Blink 65, Firefox 66 и Safari 12.1.
Пример: JavaScript для слотов
Метод assignedNodes() возвращает вставленные элементы страницы, запасное содержимое в них не входит. С flatten: true у пустого слота возвращается запасное содержимое. Вставленные элементы находятся поиском по странице, а запасное содержимое только поиском внутри теневого DOM.
Вставляемые элементы находит поиск по странице, а запасное содержимое и всё остальное внутри теневого DOM только поиск через shadowRoot компонента, если теневой DOM открыт. Событие, которое прошло через слот, содержит этот слот в composedPath(). Обработчик внутри компонента видит target без подмены, потому что кнопка относится к странице, а не к теневому DOM.
Пример: путь события через слот
Нажмите на кнопку. Событие идёт по пути кнопка, slot, обёртка внутри компонента, сам компонент, а обработчик внутри компонента видит кнопку без подмены, потому что она относится к странице.
Событие slotchange: когда слот узнаёт об изменениях
Событие slotchange сообщает, что список назначенных слоту узлов изменился.
Оно срабатывает на слоте асинхронно: после добавления узла, удаления, переноса в другой слот атрибутом slot и первого распределения, когда слот появился в теневом DOM вместе с уже добавленными детьми компонента. При переносе сообщают оба слота, прежний и новый. Изменение текста внутри уже назначенного элемента событие не вызывает, потому что список узлов тот же. Для таких изменений нужен MutationObserver. Событие есть в Blink 53, Firefox 63 и Safari 10.1.
slot.addEventListener("slotchange", () => {
const items = slot.assignedElements();
console.log(items.length);
});
Пример: журнал slotchange
Событие приходит асинхронно, после изменения. Добавление, перенос в другой слот и удаление его вызывают, а правка текста внутри уже назначенного абзаца нет. При смене slot сообщают оба слота.
Вложенные слоты и передача содержимого вниз
Слот можно записать среди дочерних элементов другого компонента, и тогда он передаёт содержимое во внутренний теневой DOM.
Если внутри теневого DOM внешнего компонента есть внутренний компонент, то среди его дочерних элементов записывают <slot name="in" slot="a">. Такой слот получает элементы внешнего компонента по своему name, а сам назначается слоту a внутреннего компонента. У внутреннего слота assignedNodes() возвращает слот-посредник, а assignedNodes({ flatten: true }) возвращает итоговый вставленный элемент. Если слот-посредник убрать, внутренний показывает своё запасное содержимое.
Пример: передача содержимого через два уровня теневого DOM
Слот-посредник записан среди дочерних элементов внутреннего компонента с атрибутом slot. Внутренний слот получает не вставляемый элемент, а этот slot, а с flatten: true возвращается итоговый вставляемый элемент. Если слота-посредника нет, показывается запасное содержимое.
Ручное назначение: slotAssignment и assign()
По умолчанию распределение идёт по названиям, но им можно управлять вручную.
Параметр slotAssignment: "manual" метода attachShadow() отключает распределение по name: узел попадает в слот только после slot.assign(узел). Узел принадлежит одному слоту, поэтому при новом вызове он пропадает из прежнего. Вызов assign() без аргументов очищает слот. Параметр работает с Blink 86, Firefox 92 и Safari 16.4, а метод assign() работает с Blink 92, Firefox 92 и Safari 16.4.
const root = host.attachShadow({
mode: "open",
slotAssignment: "manual",
});
root.querySelector("slot").assign(item);
Пример: ручное назначение
В ручном режиме названия слотов не работают: элемент попадает в слот только после вызова assign(). Элемент может быть назначен одному слоту, поэтому при переносе он пропадает из прежнего. Нужен Blink 92, Firefox 92 или Safari 16.4.
Почему тег slot не работает
Типичные причины, по которым слот ведёт себя не так, как ожидают, собраны в таблицу.
| Что видно | Причина | Что делать |
|---|---|---|
| Светлый элемент не виден | В теневом DOM нет слота с таким name или нет безымянного слота | Добавить слот или поправить атрибут slot |
| Слот в обычной разметке ничего не делает | Нет теневого DOM, а у slot display: contents | Записать слот в шаблон теневого DOM |
| Запасное содержимое не показывается | Между тегами внутри компонента есть пробелы или переносы | Записать теги вплотную |
Рамка и фон на slot не видны | У слота нет собственного блока | Добавить display: block |
::slotted не красит вложенный элемент | Селектор находит только прямых детей компонента | Покрасить через наследование или стиль страницы |
Правило ::slotted не действует | Стиль страницы для этого свойства сильнее | Изменить стиль страницы |
slotchange молчит при правке текста | Список назначенных узлов не меняется | Следить через MutationObserver |
Второй слот с тем же name пуст | Узлы получает только первый слот | Дать слотам разные названия |
Элемент со slot не выводится при manual | В ручном режиме названия не работают | Вызвать assign() |
Частые ошибки с тегом slot
Эти записи выглядят рабочими, но ведут себя иначе, чем ожидается.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
<slot> в обычной разметке | Слот ничего не распределяет, виден только его запас | Записать слот в шаблон теневого DOM |
<ul><slot></slot></ul> | Валидатор отмечает слот в ul ошибкой | Записать слот внутри li |
<div><span slot="title"> | Вложенный элемент не попадает в слот сам | Сделать его прямым ребёнком компонента |
slot.textContent = "Текст" | Меняется запасное содержимое, а не содержимое слота | Добавить элемент в сам компонент |
slot { border: 1px solid; } | Рамки нет: у слота нет блока | Добавить display: block |
::slotted(b) | Не находит b внутри назначенного абзаца | Использовать наследование или стиль страницы |
name="a" у двух слотов | Второй слот остаётся пустым | Дать слотам разные названия |
slot.assignedNodes() | Запасное содержимое в список не входит | Добавить { flatten: true } |