Тег template в HTML: как хранить и клонировать разметку

Тег template (от английского template, «шаблон») хранит кусок разметки, который браузер разобрал, но на страницу не вывел. Содержимое template не показывается, не находится поиском по документу и не загружает картинки, пока скрипт не скопирует его на страницу. Закрывающий </template> обязателен: без него шаблон забирает в себя всю остальную страницу.

Тег нужен, чтобы повторять одну и ту же разметку: карточки, строки таблицы, пункты списка. В статье разобраны свойство content, копирование и перенос, момент запуска скриптов, особый разбор строк таблицы, importNode, повторные id, вложенные шаблоны и shadowrootmode. Окна поверх страницы описаны на странице про тег dialog, раскрывающиеся блоки на страницах про тег details и тег summary, а другие теги группы в шпаргалке по тегам.

HTML
<ul id="list"></ul>

<template id="item">
  <li><b></b></li>
</template>
JavaScript
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> и </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Корень копируется вместе с хозяином методом cloneNode124 / 125 / 17.5
shadowrootdelegatesfocusФокус на хозяине переходит к первому элементу корня, который может его получить123 / 123 / 16.4
shadowrootserializableКорень попадает в результат getHTML(), если включить опцию serializableShadowRoots125 / нет / 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.

JavaScript
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. Если в элементе два таких шаблона, корень создаёт только первый.

HTML
<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
Категория
Поделиться в социальных сетях

Отправить комментарий

0 Комментарии

Отправить комментарий (0)

#buttons=(Хорошо) #days=(20)

Мы используем файлы cookie для улучшения работы сайта. Подробнее
Хорошо
×