The template Element

Markup that is parsed but not rendered, waiting to be cloned. The clean way to build repeated content in JavaScript without writing HTML as strings.

Concept

template holds markup that the browser parses and validates but does not render. Its contents are inert: images inside it are not fetched, scripts do not run, and nothing appears on the page until a script clones it and inserts the copy.

It is the answer to building repeated content in JavaScript. The alternative - assembling HTML as a string - is error prone, easy to get wrong in a way that introduces cross site scripting, and impossible for an editor to help with.

Syntax

<template id="row-template">
  <tr>
    <th scope="row" class="route"></th>
    <td class="from"></td>
    <td class="time"></td>
  </tr>
</template>
const template = document.getElementById("row-template");
const tbody    = document.querySelector("tbody");

routes.forEach((route) => {
  const row = template.content.cloneNode(true);   // true = deep clone

  row.querySelector(".route").textContent = route.number;
  row.querySelector(".from").textContent  = route.from;
  row.querySelector(".time").textContent  = route.firstBus;

  tbody.append(row);
});

Three points that matter:

  • template.content is a document fragment holding the children. The template element itself is not what you clone.
  • cloneNode(true) takes a deep copy. Without the argument you get an empty fragment.
  • Appending a fragment inserts its children and leaves the fragment empty, which is why a fresh clone is needed on each iteration.

Why not a string

// fragile and unsafe
tbody.innerHTML += `<tr><td>${route.from}</td></tr>`;

If route.from ever contains markup - because it came from a form, a database or an API - it is inserted as markup and executed. The template approach uses textContent, which inserts text as text, and the whole class of injection disappears.

innerHTML += has a second problem: it serialises and reparses the entire container on every call, destroying event listeners and losing form state.

Why not a hidden div

<div id="row-template" hidden>
  <img src="/images/large.jpg" alt="">
</div>

A hidden element is still in the DOM. That image is fetched, ids inside it are live and can collide, and any script inside it runs. A template avoids all of that: it is parsed but inert.

Example: a card list

<template id="course-card">
  <article class="card">
    <h3><a class="card-link" href="#"></a></h3>
    <p class="card-summary"></p>
    <dl>
      <div><dt>Duration</dt><dd class="card-duration"></dd></div>
      <div><dt>Intake</dt><dd class="card-intake"></dd></div>
    </dl>
  </article>
</template>

<div id="course-list" class="grid"></div>
function renderCourses(courses) {
  const template = document.getElementById("course-card");
  const list     = document.getElementById("course-list");
  const fragment = document.createDocumentFragment();

  for (const course of courses) {
    const card = template.content.cloneNode(true);
    const link = card.querySelector(".card-link");

    link.textContent = course.title;
    link.href        = `/courses/${encodeURIComponent(course.slug)}`;
    card.querySelector(".card-summary").textContent  = course.summary;
    card.querySelector(".card-duration").textContent = course.duration;
    card.querySelector(".card-intake").textContent   = course.intake;

    fragment.append(card);
  }

  list.replaceChildren(fragment);   // one insertion, one layout pass
}

Building into a fragment first and inserting once means a single layout and paint rather than one per card.

Important rules

  • Nothing inside a template renders, loads or executes.
  • Access the contents through .content.
  • Clone deeply, and clone fresh for each copy.
  • Ids inside a template are not live until it is cloned, so they will collide once several copies are inserted. Prefer classes.
  • A template may sit anywhere, including inside a table or a select, where a div would be invalid.
  • Contents are inert but still visible in the page source.

Common mistakes

  • Cloning the template element rather than its content.
  • Forgetting true in cloneNode and inserting nothing.
  • Reusing one clone in a loop, so only the last item appears.
  • Putting ids in a template and creating duplicates once cloned.
  • Using innerHTML with untrusted values instead.
  • Using a hidden div and paying for the images it loads.
  • Inserting inside the loop rather than into a fragment.

Best practices

  • Use template for anything repeated more than twice.
  • Fill it with textContent, never innerHTML.
  • Use classes as hooks, not ids.
  • Build into a fragment and insert once.
  • Keep the template next to the container it fills.
  • Encode any value interpolated into a URL.

Practice

  1. Render a list of ten items from an array using a template, inserting once.
  2. Put an image with a large file inside a hidden div and inside a template, then compare the Network panel.
  3. Clone without true and explain what appears.
  4. Set a value containing markup with textContent and then with innerHTML, and describe the difference.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →
HTML

What HTML5 Actually Means

HTML5 is not a version you upgrade to. It is the name for a shift from a frozen specification to a living standard, and for the elements and APIs that...

Read more
HTML

The canvas Element

A blank bitmap you draw on with JavaScript. Powerful for graphics and games, and invisible to every reader who cannot see the screen.

Read more
HTML

SVG in HTML

Vector graphics that scale perfectly, weigh almost nothing and can be styled with CSS. Learn inline SVG, SVG as an image, and how to make one accessib...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.