W3docs

Template Element

Learn how the HTML <template> element stores inert, reusable markup and how to clone its content with JavaScript to build dynamic UIs efficiently.

The HTML <template> element lets you declare a chunk of markup in your page that the browser parses but does not render. The content sits ready in the document, inert, until your JavaScript clones it and inserts it into the live DOM. This makes <template> the idiomatic way to define a "blueprint" for repeated UI — list rows, cards, modal dialogs — without building DOM nodes by hand or pasting HTML strings into innerHTML.

This chapter covers what makes template content special, how to clone and insert it correctly, how to wire up events on cloned nodes, and the gotchas that bite first-time users. Templates are also a building block of Web Components, where they pair naturally with custom elements and the Shadow DOM.

Understanding the Template Element

The <template> element acts as a container for storing HTML that isn't immediately rendered. What makes its content inert is the key idea:

  • It is not displayed — the markup never paints, regardless of CSS.
  • Its resources are not fetched<img>, <video>, and <script> inside a template do not load or run until the content is cloned into the live document.
  • Scripts inside it do not execute, and IDs inside it do not collide with the rest of the page until activated.

This is fundamentally different from hiding an element with display: none. A display: none element is still part of the live DOM: its images load, its scripts run, and document.getElementById finds elements inside it. Template content lives in a separate, document-fragment-backed tree, so none of that happens until you opt in by cloning it.

<body>
  <div>You won't see the template, as it's not activated using JS.</div>
  <div id="template-container">
    <template id="my-template">
      <h1>Hidden!</h1>
    </template>
  </div>
</body>

Cloning Templates for Dynamic Content

A template is only a blueprint — it produces no visible output on its own. To use it, you read its content property (a DocumentFragment), clone that fragment, fill in your data, and append the clone to the page. Because you clone for each instance, one template can produce as many independent copies as you need, each with its own data.

There are two ways to clone:

  • template.content.cloneNode(true) — clones the fragment in place. The true argument means a deep clone (include all descendants); without it you'd copy only the empty fragment.
  • document.importNode(template.content, true) — does the same deep clone but explicitly imports the nodes into the current document. This matters when the template lives in a different document (for example, content pulled in via an <iframe> or <link rel="import">). For same-document templates the two are equivalent; importNode is the safer default.
Warning

Always clone the template content before inserting it. If you append template.content directly, you move the original nodes out of the template into the DOM — the template is then empty and the next clone produces nothing. Cloning leaves the blueprint intact for reuse.

<head>
  <style>
    .card {
      border: 1px solid #ccc;
      border-radius: 5px;
      padding: 10px;
      margin: 10px;
      width: 200px;
    }
    .card h3 {
      margin: 0;
    }
  </style>
</head>
<body>
  <div id="template-container">
    <!-- Template element -->
    <template id="card-template">
      <div class="card">
        <h3 id="card-title">Title</h3>
        <p id="card-content">Content goes here...</p>
      </div>
    </template>
  </div>

  <div id="card-container">
    <!-- Cards will be inserted here -->
  </div>

  <script>
    // Data for multiple cards
    const cardData = [
      { title: 'Card 1', content: 'This is the first card.' },
      { title: 'Card 2', content: 'This is the second card.' },
      { title: 'Card 3', content: 'This is the third card.' }
    ];

    // Function to create and insert cards
    function createCards(data) {
      const template = document.getElementById('card-template');

      data.forEach(item => {
        const clone = document.importNode(template.content, true);

        // Customize the cloned content
        clone.querySelector('#card-title').textContent = item.title;
        clone.querySelector('#card-content').textContent = item.content;

        // Insert the cloned content into the DOM
        document.getElementById('card-container').appendChild(clone);
      });
    }

    // Create cards with the provided data
    createCards(cardData);
  </script>
</body>

Enhancing Interactivity with JavaScript

The content property of a template returns a DocumentFragment that holds the inert markup. You can query and read it (template.content.querySelector(...)) without touching the live page, but a cleaner pattern is to clone first and then wire up events on the clone — that way each instance gets its own listeners. For deeper coverage of attaching handlers, see Event Handling in the DOM.

The example below defines two templates: a button and a content card. Clicking the button clones the card template and appends a fresh copy each time.

<body>
  <div id="template-container">
    <!-- Button Template -->
    <template id="button-template">
      <button id="show-content-btn">Add a content card</button>
    </template>

    <!-- Content Template -->
    <template id="content-template">
      <div class="content">
        <h2>Dynamic Content</h2>
        <p>This content is added dynamically when the button is clicked.</p>
      </div>
    </template>
  </div>

  <div id="button-container">
    <!-- Button will be inserted here -->
  </div>

  <div id="content-container">
    <!-- Content will be displayed here -->
  </div>

  <script>
    // Function to display template content
    function displayTemplateContent() {
      // Get the content template
      const contentTemplate = document.getElementById('content-template');
      // Access the .content property and clone it
      const contentClone = document.importNode(contentTemplate.content, true);

      // Display the cloned content
      document.getElementById('content-container').appendChild(contentClone);
    }

    // Insert the button template into the DOM
    function insertButton() {
      // Get the button template
      const buttonTemplate = document.getElementById('button-template');
      const buttonClone = document.importNode(buttonTemplate.content, true);

      // Add event listener to the button
      buttonClone.querySelector('#show-content-btn').addEventListener('click', displayTemplateContent);

      // Insert the button into the DOM
      document.getElementById('button-container').appendChild(buttonClone);
    }

    // Call the function to insert the button when the page loads
    insertButton();
  </script>
</body>

In this example:

  • We have two templates: one for a button (button-template) and one for content (content-template).
  • The insertButton function clones the button template and inserts it into the DOM. It also attaches an event listener to the button to call the displayTemplateContent function when clicked.
  • The displayTemplateContent function clones the content template and inserts it into the DOM each time the button is clicked.
  • The button is inserted into the DOM when the page loads by calling insertButton.

Thus, when you click on the "Try it yourself" button, you'll see a button labeled "Add a content card." Each time you click it, a new card from the content template is added to the page, demonstrating dynamic insertion driven by user interaction.

Common Gotchas

A few pitfalls account for most template bugs:

  • Forgetting to clone. Appending template.content directly empties the template. Always clone first.
  • Querying after appending. appendChild(clone) empties the fragment into the DOM, so the clone's nodes become children of the target. Read or modify the clone (clone.querySelector(...)) before you append it — afterward the fragment is empty.
  • Duplicate IDs. IDs written inside a template are fine while inert, but once you clone it many times, each copy carries the same ID — and duplicate IDs are invalid HTML. Prefer classes, data-* attributes, or scoped queries on the clone rather than document.getElementById for per-instance lookups.
  • Expecting scripts to run. A <script> inside a template does not execute on clone unless you re-create it. Keep behavior in your JavaScript, not in template markup.

Browser Support

The <template> element is part of the HTML Living Standard and is supported in every modern browser (Chrome, Firefox, Safari, Edge). No polyfill is needed for current browsers, which makes it a safe, dependency-free choice for client-side templating.

Conclusion

The <template> element gives you a native, framework-free way to define reusable markup and instantiate it on demand. Because its content is inert until cloned, it avoids the wasted rendering and resource loading of hidden elements, and it sidesteps the security and parsing quirks of building HTML from strings. Clone with cloneNode(true) or importNode, populate the clone, then append — and you have an efficient pattern that scales from a single card to a whole component system. To see templates inside a full component, continue with Custom Elements and Web Components.

Practice

Practice
What is the primary purpose of the Template Element in JavaScript?
What is the primary purpose of the Template Element in JavaScript?
Was this page helpful?