Etiqueta template en HTML: contenido inerte y Shadow DOM
La etiqueta <template> conserva una plantilla de HTML cuyo contenido no se representa directamente al cargar el documento en su uso ordinario. Sirve para preparar una estructura que JavaScript copiará e insertará después: por ejemplo, una tarjeta de curso que se repetirá con distintos títulos y descripciones. Úsala cuando necesitas reutilizar ese marcado; si el contenido debe aparecer desde el principio, escríbelo directamente en la página. El Shadow DOM declarativo es otro uso de <template>, con un mecanismo diferente.
Ejemplo mínimo: una tarjeta que todavía no se ve
<template id="tarjeta-curso">
<article class="tarjeta"><h2></h2><p></p></article>
</template>
Este fragmento no muestra una tarjeta, ni un encabezado vacío, ni un párrafo. El navegador analiza el marcado y lo guarda para usarlo después. Cambiar el CSS de .tarjeta no lo inserta en la página.
Para comprobarlo, crea minimo.html con un documento HTML que tenga <!doctype html>, <html lang="es">, un <head> con codificación UTF-8 y título, y un <body>. Copia allí el fragmento y añade debajo este destino:
<div id="resultado"></div>
<p>La tarjeta aparecerá cuando se ejecute el script.</p>
Después del destino, antes de </body>, añade:
<script>
const plantilla = document.querySelector("#tarjeta-curso");
const copia = plantilla.content.cloneNode(true);
copia.querySelector("h2").textContent = "HTML inicial";
copia.querySelector("p").textContent = "Aprende a estructurar una página.";
document.querySelector("#resultado").append(copia);
</script>
Al abrir el archivo con JavaScript habilitado aparece una tarjeta titulada «HTML inicial», seguida de su descripción. Sin JavaScript queda el párrafo explicativo y el destino vacío. Los scripts de estos bloques son código de estudio: no se ejecutan dentro de esta guía.
Sintaxis, contexto y la propiedad content
Escribe ambas etiquetas, <template> y </template>: no se permite omitirlas. <template /> no cierra este elemento en HTML. Puede colocarse donde se admita contenido de metadatos, de frase o elementos de apoyo a scripts; por ejemplo, en <head>, <body> y ciertos contextos de tabla. También se admite dentro de <colgroup> sin atributo span. No cambia las restricciones del lugar donde insertarás la copia: una plantilla de filas debe acabar en un contexto de tabla válido.
La propiedad JavaScript .content devuelve un DocumentFragment, un contenedor de nodos sin elemento envolvente. Lo escrito entre las etiquetas se analiza como contenido de ese fragmento, no como hijos normales del elemento <template>. Por eso el estándar define su modelo de contenido como «nada», aunque la sintaxis contenga marcado para la plantilla. Consulta el modelo y contexto de <template> en HTML.
En el ejemplo, plantilla.querySelector("h2") devuelve null; plantilla.content.querySelector("h2") encuentra el encabezado. Añadir hijos con plantilla.append(...) no equivale a modificar .content y vulnera ese modelo. Para trabajar con la plantilla usa el fragmento, manteniendo válidas las estructuras que contiene.
Cómo clonar e insertar una plantilla
El ejemplo mínimo sigue cuatro pasos: localizar el elemento, copiar su contenido, completar la copia e insertarla. En cloneNode(true), true pide una copia profunda que incluye los descendientes. Con false, el fragmento copiado queda vacío.
append(copia) mueve los nodos del fragmento al destino: no crea una etiqueta DocumentFragment. Después, copia queda vacía, pero la plantilla original sigue disponible. En cambio, insertar plantilla.content directamente consume su contenido y deja de servir para producir nuevas tarjetas. La referencia de DocumentFragment explica esta diferencia.
Clonar copia atributos como id, pero no los manejadores registrados con addEventListener(). Si añades botones, registra sus eventos en cada copia o usa delegación en el contenedor. La referencia de cloneNode() detalla estas restricciones. Para texto procedente de datos utiliza textContent; no necesitas interpretarlo como HTML.
Caso práctico: tres cursos desde datos locales
Guarda este documento completo como cursos.html y ábrelo en el navegador. No necesita imágenes, bibliotecas, solicitudes de red ni un servidor. La lista inicial ofrece los mismos datos si el script está deshabilitado o falla; solo se oculta cuando las tres tarjetas se han insertado.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cursos disponibles</title>
<style>
body { font-family: sans-serif; margin: 1rem; }
#cursos { display: grid; gap: 1rem; }
.tarjeta { border: 1px solid #777; padding: 1rem; }
</style>
</head>
<body>
<h1>Cursos disponibles</h1>
<ul id="alternativa">
<li>HTML inicial: estructura de una página.</li>
<li>Formularios accesibles: nombres e instrucciones.</li>
<li>Tablas de datos: encabezados y asociaciones.</li>
</ul>
<div id="cursos"></div>
<template id="tarjeta-curso">
<article class="tarjeta">
<h2></h2>
<p></p>
</article>
</template>
<script>
const datos = [
{ titulo: "HTML inicial", descripcion: "Estructura de una página." },
{ titulo: "Formularios accesibles", descripcion: "Nombres e instrucciones." },
{ titulo: "Tablas de datos", descripcion: "Encabezados y asociaciones." }
];
const plantilla = document.querySelector("#tarjeta-curso");
const destino = document.querySelector("#cursos");
const tarjetas = document.createDocumentFragment();
for (const curso of datos) {
const copia = plantilla.content.cloneNode(true);
copia.querySelector("h2").textContent = curso.titulo;
copia.querySelector("p").textContent = curso.descripcion;
tarjetas.append(copia);
}
destino.append(tarjetas);
document.querySelector("#alternativa").hidden = true;
</script>
</body>
</html>
Con JavaScript aparecen tres tarjetas en el orden del array, cada una con su encabezado y descripción. No hay IDs dentro de la estructura clonada, así que las copias no generan identificadores duplicados. El id de <template> identifica únicamente el original. Si necesitas relacionar controles y etiquetas dentro de una tarjeta, genera IDs únicos para cada copia.
Deshabilita JavaScript y recarga: verás la lista de tres cursos. Quita el <script> para provocar el mismo resultado. Si cambias un título en datos, cambia también la alternativa estática para mantener ambos resultados coherentes. Retirar CSS conserva los títulos y el orden; reemplazar <template> por <div> muestra su estructura directamente y rompe .content. Si eliminas la plantilla, el script falla al buscarla, pero la lista inicial permanece legible.
Plantillas y Shadow DOM declarativo
<template shadowrootmode="open"> indica al analizador HTML que cree un árbol de sombra en su elemento padre, si este puede alojarlo y todavía no tiene uno. No se obtiene una tarjeta mediante .content.cloneNode(): el contenido pasa a la raíz de sombra y el <template> se consume. La presentación resultante puede aparecer sin JavaScript.
Prueba este segundo documento independiente como sombra.html:
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Curso con árbol de sombra</title>
<style>
body { font-family: sans-serif; margin: 1rem; }
p { color: #333; }
</style>
</head>
<body>
<h1>Oferta de formación</h1>
<section id="curso">
<template shadowrootmode="open">
<style>
:host { display: block; border: 1px solid #777; padding: 1rem; }
p { color: #173e75; }
</style>
<slot name="titulo"></slot>
<p>Material de práctica incluido.</p>
<slot></slot>
</template>
<h2 slot="titulo">HTML inicial</h2>
<p>Cuatro sesiones para aprender a estructurar una página.</p>
</section>
</body>
</html>
En un navegador compatible, el curso aparece con borde y el aviso «Material de práctica incluido». Los hijos del anfitrión se distribuyen mediante <slot>: el h2 va al punto llamado titulo y el párrafo al punto sin nombre. Permanecen en el DOM de la página; no son copias. La guía de <slot> explica esa distribución.
El selector p del documento no entra en la raíz de sombra: el aviso interno usa su regla azul. El párrafo distribuido sí sigue recibiendo el CSS exterior. Hay herencia de propiedades, como la fuente, a través del anfitrión; Shadow DOM no equivale a un documento completamente aislado. Consulta la explicación de estilos y Shadow DOM.
Sin soporte, el <template> queda inerte, pero el título y el párrafo exteriores permanecen visibles. Para simularlo cambia shadowrootmode por un atributo desconocido y recarga: desaparecen el borde y el aviso interno. Esa simulación comprueba la alternativa, no sustituye una prueba en un navegador antiguo. Quitar solo las etiquetas de <template> dejaría su estilo y aviso en el documento ordinario: el CSS podría afectar a otros párrafos.
No cualquier padre admite una raíz de sombra; aquí usamos <section>, que sí la admite. Un valor inválido de shadowrootmode, o un segundo intento en un anfitrión que ya tiene raíz, deja una plantilla ordinaria. Añadir el atributo después con JavaScript no transforma una plantilla ya analizada. Tampoco basta asignar este HTML mediante innerHTML para crear una raíz declarativa.
Atributos y accesibilidad
La clonación ordinaria no necesita atributos específicos: id y class son globales; .content, cloneNode() y textContent son API de JavaScript; display es CSS. Los atributos siguientes corresponden al Shadow DOM declarativo:
shadowrootmode="open"o"closed": crea una raíz abierta o cerrada. Sin atributo o con valor inválido no se solicita la raíz. En modo abierto,anfitrion.shadowRootpermite acceder a ella; en modo cerrado devuelvenull, aunque el contenido se renderice.shadowrootdelegatesfocus: booleano; activa delegación de foco hacia un descendiente enfocable. No añade nombres accesibles a los controles.shadowrootclonable: booleano; permite incluir la raíz al clonar el anfitrión. No controla la copia de.contentde una plantilla ordinaria.shadowrootserializable: booleano; permite incluir la raíz en una serialización solicitada mediantegetHTML()con las opciones correspondientes.shadowrootslotassignment:namedes el valor predeterminado;manualrequiere asignación mediante la API de slots. El ejemplo usa la distribución predeterminada.shadowrootcustomelementregistry: booleano; deja pendiente el registro de elementos personalizados de la raíz para inicializar un registro acotado. No es necesario en estos ejemplos.
Los booleanos se activan por presencia: shadowrootclonable="false" sigue activado. Sin ellos, las opciones booleanas descritas quedan desactivadas. El estándar también define for para actualizar contenido mediante marcadores; no participa en la clonación enseñada aquí y su soporte es distinto.
Una plantilla inerte no ofrece al usuario su contenido. Conserva los datos esenciales fuera de ella y, al insertar las copias, usa encabezados y controles nativos en un orden de lectura lógico. <template> no necesita un role ARIA. Una raíz closed no es una barrera de seguridad ni impide por sí misma el acceso de las tecnologías de asistencia. En componentes con controles, comprueba nombres, Tab y foco dentro de la raíz; no supongas que las referencias a IDs cruzan libremente sus límites.
Errores frecuentes y preguntas
¿Por qué no se ve el contenido?
En una plantilla ordinaria es el resultado esperado hasta insertarlo. Si ya escribiste un script, revisa que encuentre el id, que se ejecute después de crear el destino, que copie .content con true y que inserte la copia en un elemento visible. Consulta los errores de la consola antes de intentar corregirlo con CSS.
¿Template necesita siempre JavaScript?
La clonación ordinaria mostrada requiere lógica JavaScript. Shadow DOM declarativo puede crear su estructura durante el análisis de HTML sin scripts, siempre que haya soporte. No son dos formas intercambiables de ocultar y mostrar una tarjeta.
¿Puede usarse como sanitizador o como plantilla de framework?
No. La inercia no elimina atributos peligrosos ni vuelve seguro un fragmento recibido de otra persona: al insertarlo cambia su situación. Evita innerHTML para los textos del ejemplo. Tampoco hay interpolación automática: escribir {{titulo}} dentro de un <template> HTML no sustituye valores; las plantillas de frameworks dependen de sus propias herramientas.
Para contenido visible desde el inicio usa un contenedor apropiado, como <div>, o <article> si la tarjeta constituye una pieza independiente. <template> prepara la estructura reutilizable y <script> aporta la lógica del ejemplo; ninguna de esas funciones sustituye la semántica de la tarjeta.
Compatibilidad y fuentes
Compatibilidad consultada el 4 de octubre de 2026 en los datos de MDN para <template>. El elemento ordinario se admite desde Chrome 26, Firefox 22 y Safari 8. shadowrootmode tiene su propia cobertura: Chrome 111, Firefox 123 y Safari 16.4. Usa el nombre vigente shadowrootmode; el antiguo shadowroot no es la sintaxis actual.
No extrapoles ese soporte a todos los atributos: shadowrootclonable figura desde Chrome 124, Firefox 125 y Safari 17.5; shadowrootserializable, desde Chrome 125 y Safari 18, sin soporte en Firefox en los datos consultados. shadowrootslotassignment figura en Firefox 151 y no en Chrome ni Safari; shadowrootcustomelementregistry, en Chrome 146 y Safari 26, con Firefox en vista previa. for figura en Chrome 150 y no en Firefox ni Safari. MDN recoge además shadowrootreferencetarget como experimental y fuera del estándar; no se usa en esta guía.
Contrasta cada opción adicional antes de incorporarla. La referencia de MDN complementa las restricciones normativas enlazadas y los detalles de cada API. Los ejemplos no dependen de las opciones avanzadas: las tarjetas usan la clonación ordinaria y el documento declarativo conserva sus datos básicos si la raíz no se crea.