Тег template (от английского template, «шаблон») хранит кусок разметки, который браузер разобрал, но на страницу не вывел. Содержимое template не показывается, не находится поиском по документу и не загружает картинки, пока скрипт не скопирует его на страницу. Закрывающий </template> обязателен: без него шаблон забирает в себя всю остальную страницу.
Тег нужен, чтобы повторять одну и ту же разметку: карточки, строки таблицы, пункты списка. В статье разобраны свойство content, копирование и перенос, момент запуска скриптов, особый разбор строк таблицы, importNode, повторные id, вложенные шаблоны и shadowrootmode. Окна поверх страницы описаны на странице про тег dialog, раскрывающиеся блоки на страницах про тег details и тег summary, а другие теги группы в шпаргалке по тегам.
<ul id="list"></ul>
<template id="item">
<li><b></b></li>
</template>
const tpl = document.getElementById("item");
const list = document.getElementById("list");
for (const name of ["Анна", "Борис"]) {
const copy = tpl.content.cloneNode(true);
copy.querySelector("b").textContent = name;
list.appendChild(copy);
}
В этой статье
- Тег template в HTML: что это и как устроено содержимое
- Атрибуты тега template: shadowrootmode и другие
- Как добавить шаблон на страницу: cloneNode или перенос
- Когда содержимое template начинает работать
- Строки таблицы и особый разбор внутри template
- importNode или cloneNode: пользовательские элементы
- Повторные id, события и вложенные template
- Declarative Shadow DOM: shadowrootmode у template
- Тег template во Vue и других шаблонизаторах
- Почему тег template не работает
Тег template в HTML: что это и как устроено содержимое
Сначала о том, где хранится разметка шаблона и почему страница её не видит.
Браузер разбирает всё, что записано между <template> и </template>, но помещает элементы не в документ, а в отдельный фрагмент. Он доступен как свойство content и относится к другому документу: его ownerDocument не совпадает с document. Поэтому у самого template дочерних элементов нет. У шаблона из двух абзацев template.children.length равен 0, а template.content.children.length равен 2.
Поэтому document.querySelector элементы шаблона не находит, стили страницы на них не действуют, а программы чтения экрана их пропускают. Даже display: block у template ничего не показывает: высота остаётся нулевой. Свойство content только для чтения: в обычном скрипте присвоение в него игнорируется, а в строгом режиме вызывает TypeError. Изменить содержимое можно через template.innerHTML или методы фрагмента.
Пример: что видит страница
Пока копия не добавлена, на странице нет ни одного абзаца, хотя в шаблоне их два. Кнопка добавляет копию, и счётчик на странице растёт, а в шаблоне остаётся по два элемента.
Пробелы и переносы строк между тегами тоже попадают во фрагмент, но как текстовые узлы. Если два абзаца записаны на отдельных строках, то content.childNodes.length равен 5, children.length равен 2, а firstChild оказывается пустым текстом, а не абзацем. Для первого элемента берут firstElementChild.
Валидатор принимает template в head, body, table, tbody, tr, ul, select, colgroup, dl, p и button. Внутри самого шаблона тег body недопустим.
Атрибуты тега template: shadowrootmode и другие
У обычного шаблона своих атрибутов нет, а все специальные относятся к теневому DOM.
| Атрибут | Что делает | Blink / Firefox / Safari |
|---|---|---|
shadowrootmode | Значение open или closed превращает шаблон в теневой корень родителя при разборе страницы | 111 / 123 / 16.4 |
shadowrootclonable | Корень копируется вместе с хозяином методом cloneNode | 124 / 125 / 17.5 |
shadowrootdelegatesfocus | Фокус на хозяине переходит к первому элементу корня, который может его получить | 123 / 123 / 16.4 |
shadowrootserializable | Корень попадает в результат getHTML(), если включить опцию serializableShadowRoots | 125 / нет / 18 |
id | Обычный глобальный атрибут, по нему находят шаблон: getElementById("item") | везде |
Без shadowrootmode шаблон остаётся хранилищем и ничего больше не делает. Значение none недопустимо, валидатор отмечает его ошибкой. Подробности в разделе про теневой DOM.
Как добавить шаблон на страницу: cloneNode или перенос
Содержимое шаблона само на странице не появляется, его копируют или переносят.
Обычный способ такой: template.content.cloneNode(true) делает копию фрагмента, а appendChild добавляет её на страницу. Параметр true нужен, чтобы скопировать вложенные элементы, без него получится пустой фрагмент. Шаблон при этом не меняется, поэтому копий можно сделать сколько угодно.
Если вызвать appendChild(template.content) без клонирования, узлы переносятся: в шаблоне их становится 0, и повторный вызов ничего не добавит. После добавления клона сам клон тоже пуст, поэтому данные в него записывают до добавления, а не после.
Пример: копия и перенос
Нажмите «cloneNode(true) и добавить» несколько раз: на странице появляются новые копии, а шаблон не меняется. После «Добавить сам content» абзацы переходят на страницу, в шаблоне их остаётся 0, и повторное нажатие ничего не добавляет.
Метод template.cloneNode(true) копирует сам элемент вместе с содержимым, а template.cloneNode(false) даёт пустой шаблон.
Данные в клон записывают до добавления: находят элемент через querySelector и меняют его textContent. Свойство textContent выводит строку как текст, а innerHTML разбирает её как разметку. Для строк, которые ввёл пользователь, нужен textContent.
Пример: карточки из данных
В третьей записи данных есть теги. Запись textContent выводит их символами, запись innerHTML разбирает как разметку и создаёт курсивный элемент. Для строк от пользователей нужен textContent.
Когда содержимое template начинает работать
Пока шаблон не скопирован на страницу, всё внутри него неактивно.
Картинки и iframe из шаблона не загружаются: к их адресам нет обращений, пока копия не окажется в документе. Скрипт внутри шаблона при разборе страницы не выполняется. Он выполняется один раз для каждой копии, добавленной методом appendChild или importNode, поэтому две копии дают два запуска. Это верно для шаблона, записанного в разметке страницы: если его заполнили через innerHTML, скрипт не выполнится ни в одной копии. Правила style из шаблона начинают действовать после добавления копии. Поля формы из шаблона не попадают в FormData, пока не окажутся внутри формы.
Запись element.innerHTML = template.innerHTML работает иначе: разметка разбирается заново, стили действуют, а скрипты из неё не выполняются.
Пример: когда содержимое начинает работать
Скрипт из шаблона выполняется, когда копия добавлена на страницу методом appendChild, и не выполняется при записи через innerHTML. Стили в обоих случаях действуют. Скрытое поле формы попадает в данные формы только после добавления.
Строки таблицы и особый разбор внутри template
Внутри template разбор разметки мягче, чем внутри div: допустимы теги, которые в других местах браузер выбрасывает.
Если записать <tr><td>1</td></tr> в div.innerHTML, строка таблицы не появится: браузер отбросит теги и оставит только текст. В template те же теги сохраняются, поэтому шаблон подходит для строк и ячеек таблицы. Для строки из двух ячеек template.content.querySelectorAll("td").length даёт 2.
Сам template можно записать прямо внутри table, tbody или tr. Таблица от этого не ломается: у неё остаются дочерние template и tbody.
const row = document.getElementById("row");
const tbody = document.querySelector("#table tbody");
tbody.appendChild(row.content.cloneNode(true));
Пример: строка таблицы
Шаблон хранит строку целиком и добавляет её в таблицу. Запись той же строки в div.innerHTML отбрасывает теги tr и td: остаётся только текст ячеек без разделения.
importNode или cloneNode: пользовательские элементы
Оба метода копируют узлы, но по-разному обращаются с пользовательскими элементами и документом-владельцем.
Клон cloneNode(true) принадлежит документу шаблона, поэтому пользовательский элемент внутри него не обновляется до своего класса, пока клон не окажется на странице. Метод document.importNode(template.content, true) сразу делает копию частью document, и пользовательский элемент обновляется до добавления. После добавления на страницу обновляются оба варианта.
Если пользовательский элемент нужно настроить до добавления, берут importNode. В остальных случаях разницы нет, и проще cloneNode.
Пример: cloneNode и importNode
Клон из cloneNode до добавления на страницу ещё не обновлён до класса и принадлежит документу шаблона. Копия из importNode принадлежит документу страницы и обновляется сразу. После добавления на страницу различий нет.
Повторные id, события и вложенные template
Копии шаблона получают одинаковые атрибуты, а обработчики фрагмента после добавления теряются.
Если в шаблоне записан id, каждая копия добавляет такой же элемент, и на странице оказывается несколько элементов с одним id. Метод getElementById вернёт только первый, а id по правилам HTML должен быть уникальным. Поэтому внутри шаблона используют class или data-*, а нужный элемент ищут в клоне до добавления.
Обработчик, назначенный фрагменту через addEventListener, после appendChild теряется: фрагмент пуст и в документе его нет, поэтому событие до него не доходит. Обработчик на дочернем элементе или на общем родителе работает. Атрибут onclick в разметке шаблона тоже работает после добавления.
Пример: повторные id и обработчики
Каждая копия добавляет кнопку с тем же id. Нажмите на добавленную кнопку: обработчик, назначенный фрагменту, после добавления не вызывается, а обработчик на самой кнопке работает.
Шаблон внутри шаблона остаётся шаблоном: поиск outer.content.querySelectorAll("b") не видит элементы вложенного, они находятся в его собственном content. Копия внешнего фрагмента при этом переносит и вложенный template вместе с содержимым, а нужные элементы из него достают отдельным шагом.
Пример: шаблон в шаблоне
Тег b находится только во вложенном шаблоне, поэтому поиск по внешнему content его не находит. Копия внешнего шаблона приносит на страницу вложенный template целиком, и тег b на странице появляется после второго шага.
Declarative Shadow DOM: shadowrootmode у template
Атрибут shadowrootmode превращает шаблон в теневой корень родителя, но только если разметку разбирает сама страница.
Теневой DOM прячет внутренности элемента: стили корня наружу не выходят, а стили страницы внутрь не попадают. Если записать <template shadowrootmode="open"> внутри элемента, то при разборе страницы браузер создаёт у этого элемента shadowRoot, а сам template из дочерних элементов исчезает. При значении closed корень создаётся, но element.shadowRoot возвращает null. Если в элементе два таких шаблона, корень создаёт только первый.
<div id="host">
<template shadowrootmode="open">
<style>p { color: #C0392B; }</style>
<p>Текст в теневом корне</p>
</template>
</div>
Разметка, добавленная скриптом, корень не создаёт: после innerHTML, insertAdjacentHTML, createContextualFragment и DOMParser шаблон остаётся обычным тегом с атрибутом. Корень создают методы setHTMLUnsafe() и Document.parseHTMLUnsafe(). Первый доступен во всех основных браузерах с сентября 2025 года, второй есть в новых версиях. Хозяин корня должен быть родителем шаблона, поэтому в строке нужен внешний элемент. Слово Unsafe в названии означает, что разметка не очищается от опасных элементов и атрибутов, поэтому строки из внешних источников в эти методы не передают.
Элемент из parseHTMLUnsafe() сохраняет корень при appendChild, а importNode его теряет. Если такой template записан внутри обычного шаблона, корень создаётся уже в его content, а обычная копия его теряет. Корень переживает копирование, только если указан shadowrootclonable.
Пример: способы добавить теневой корень
Одна и та же строка с template и shadowrootmode добавляется разными способами. Красный текст появляется только там, где браузер создал теневой корень: у innerHTML, insertAdjacentHTML и DOMParser шаблон остаётся обычным тегом.
Тег template во Vue и других шаблонизаторах
Тег с таким же названием встречается в файлах Vue, но это другой объект.
В однофайловых компонентах Vue (файлы .vue) блок <template> описывает разметку компонента. Компилятор заранее превращает его в JavaScript, и в итоговой странице такого элемента нет. Поэтому это не HTML-тег, а раздел файла. Шаблон из этой статьи с Vue не связан и работает без библиотек.
Почему тег template не работает
Типичные причины, по которым шаблон ведёт себя не так, как ожидают, собраны в таблицу.
| Что видно | Причина | Что делать |
|---|---|---|
| Шаблон ничего не выводит | Содержимое скрыто всегда, даже при display: block | Скопировать content на страницу |
template.children пуст, поиск ничего не находит | Элементы находятся в template.content | Искать через template.content |
firstChild оказался не элементом | Пробелы между тегами стали текстовыми узлами | Взять firstElementChild |
| Копия добавляется один раз, потом пусто | На страницу добавлен сам content, а не копия: узлы переносятся | Добавлять клон: cloneNode(true) |
| Скрипт из шаблона не запустился | Разметка записана через innerHTML | Добавить клон методом appendChild |
| Строка таблицы превратилась в текст | Запись в div.innerHTML отбрасывает tr и td | Взять строку из template |
| Обработчик на копии не срабатывает | Он назначен фрагменту, а не элементу | Назначить его элементу или родителю |
| Остальная страница пропала | Нет закрывающего </template> | Закрыть шаблон |
shadowrootmode не создаёт корень | Разметка добавлена через innerHTML | Взять setHTMLUnsafe() или разметку |
Частые ошибки с тегом template
Эти записи выглядят рабочими, но ведут себя иначе, чем ожидается.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
<template>… | Нет закрывающего тега: шаблон забирает остальную страницу, валидатор отмечает ошибку | Закрыть тег </template> |
tpl.firstElementChild | Возвращается null: элементы находятся не в самом шаблоне | Искать в tpl.content |
tpl.content.firstChild | Первым узлом оказывается пустой текст | Взять firstElementChild |
div.innerHTML со строкой tr | Теги tr и td пропадают, остаётся текст | Хранить строку в template |
el.innerHTML = text | Данные разбираются как разметка | Использовать textContent |
<li id="row"> | Каждая копия повторяет id | Заменить на class |
shadowrootmode="none" | Недопустимое значение, валидатор отмечает ошибку | Записать open или closed |
<template><body>… | Тег body внутри шаблона недопустим | Убрать body |