JavaScript Tagged Template Literals Deep Dive

A deep dive into JavaScript tagged template literals. Covers tag function anatomy, the strings and values parameters, raw strings, building custom HTML sanitizers, i18n formatters, CSS-in-JS helpers, SQL query builders, and advanced patterns for DSL creation with template tags.

JavaScriptintermediate
12 min read

Tagged template literals let you process a template string with a custom function. The tag function receives the static string parts and the interpolated values as separate arguments, giving you full control over how the final output is assembled. This is the mechanism behind libraries like styled-components, graphql-tag, and html template engines.

How Tag Functions Work

A tag function is called with the template literal placed after its name, with no parentheses:

javascriptjavascript
function tag(strings, ...values) {
  console.log(strings); // Array of static string parts
  console.log(values);  // Array of interpolated values
}
 
const name = "Alice";
const age = 30;
 
tag`Hello, ${name}! You are ${age} years old.`;
// strings: ["Hello, ", "! You are ", " years old."]
// values:  ["Alice", 30]

The strings array always has one more element than values. The static parts surround the interpolated expressions.

Building the Output

Most tag functions iterate strings and values to produce a result:

javascriptjavascript
function highlight(strings, ...values) {
  let result = "";
  strings.forEach((str, i) => {
    result += str;
    if (i < values.length) {
      result += `**${values[i]}**`;
    }
  });
  return result;
}
 
const item = "JavaScript";
const count = 42;
 
highlight`Learn ${item} in ${count} tutorials`;
// "Learn **JavaScript** in **42** tutorials"

The strings.raw Property

The strings parameter has a raw property containing the raw (unescaped) versions of each string part:

javascriptjavascript
function showRaw(strings) {
  console.log("cooked:", strings[0]); // interprets \n as newline
  console.log("raw:",    strings.raw[0]); // keeps \n as literal characters
}
 
showRaw`Hello\nWorld`;
// cooked: Hello
// World
// raw: Hello\nWorld

JavaScript provides a built-in String.raw tag that returns the raw string:

javascriptjavascript
const path = String.raw`C:\Users\alice\documents`;
// "C:\\Users\\alice\\documents" โ€” backslashes preserved

Practical Pattern: HTML Sanitizer

Prevent XSS by escaping interpolated values:

javascriptjavascript
function safeHtml(strings, ...values) {
  const escape = (str) =>
    String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
 
  let result = "";
  strings.forEach((str, i) => {
    result += str;
    if (i < values.length) {
      result += escape(values[i]);
    }
  });
  return result;
}
 
const userInput = '<script>alert("xss")</script>';
const html = safeHtml`<div class="comment">${userInput}</div>`;
// '<div class="comment">&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</div>'

The static template parts are trusted (written by the developer), while interpolated values are escaped.

Practical Pattern: SQL Query Builder

Build parameterized queries to prevent SQL injection:

javascriptjavascript
function sql(strings, ...values) {
  let query = "";
  const params = [];
 
  strings.forEach((str, i) => {
    query += str;
    if (i < values.length) {
      params.push(values[i]);
      query += `$${params.length}`;
    }
  });
 
  return { query, params };
}
 
const userId = 42;
const status = "active";
 
const result = sql`SELECT * FROM users WHERE id = ${userId} AND status = ${status}`;
// { query: "SELECT * FROM users WHERE id = $1 AND status = $2", params: [42, "active"] }
ApproachInjection Safe?Readable?
String concatenationNoSomewhat
Parameterized query (manual)YesVerbose
Tagged template SQLYesVery readable

Practical Pattern: i18n Formatter

Format localized strings with automatic number and date formatting:

javascriptjavascript
function i18n(strings, ...values) {
  const locale = "en-US";
  let result = "";
 
  strings.forEach((str, i) => {
    result += str;
    if (i < values.length) {
      const val = values[i];
      if (typeof val === "number") {
        result += new Intl.NumberFormat(locale).format(val);
      } else if (val instanceof Date) {
        result += new Intl.DateTimeFormat(locale).format(val);
      } else {
        result += String(val);
      }
    }
  });
  return result;
}
 
const price = 1234567.89;
const date = new Date("2026-03-06");
 
i18n`Total: ${price} as of ${date}`;
// "Total: 1,234,567.89 as of 3/6/2026"

Practical Pattern: CSS-in-JS

Create scoped CSS strings with dynamic values:

javascriptjavascript
function css(strings, ...values) {
  let result = "";
  strings.forEach((str, i) => {
    result += str;
    if (i < values.length) {
      const val = values[i];
      result += typeof val === "number" ? `${val}px` : val;
    }
  });
  return result.trim();
}
 
const padding = 16;
const color = "#3b82f6";
 
const styles = css`
  .card {
    padding: ${padding};
    color: ${color};
    border-radius: ${8};
  }
`;
// ".card { padding: 16px; color: #3b82f6; border-radius: 8px; }"

Returning Non-Strings

Tag functions can return any type, not just strings:

javascriptjavascript
function toArray(strings, ...values) {
  const result = [];
  strings.forEach((str, i) => {
    if (str) result.push(str.trim());
    if (i < values.length) result.push(values[i]);
  });
  return result;
}
 
toArray`name: ${"Alice"}, age: ${30}`;
// ["name:", "Alice", ", age:", 30]

Libraries use this to return AST nodes, DOM elements, or framework-specific objects.

Nesting Tagged Templates

Tag functions compose naturally:

javascriptjavascript
const header = safeHtml`<h1>${title}</h1>`;
const body   = safeHtml`<p>${content}</p>`;
 
const page = safeHtml`
  <div class="page">
    ${header}
    ${body}
  </div>
`;

Each inner tag processes its own interpolations, and the outer tag receives the already-processed result.

Comparison With Regular Template Literals

FeatureRegular Template LiteralTagged Template Literal
Syntax`Hello $\{name\}`tag`Hello $\{name\}`
Result typeAlways a stringAny type (tag decides)
InterpolationAutomatic toString()Tag controls processing
Use caseSimple string formattingSanitization, DSLs, parsing
Raw accessNot availablestrings.raw
Rune AI

Rune AI

Key Insights

  • Tag functions receive strings and values separately: Static parts are in the strings array; interpolated expressions are in the values rest parameter
  • strings.raw provides unescaped string parts: Useful for paths, regex patterns, and any content where backslashes should be preserved
  • Tag functions can return any type: Strings, arrays, DOM nodes, AST objects, or framework-specific values
  • HTML sanitization is the classic use case: Escape interpolated user input while trusting the developer-written template structure
  • The strings reference is stable per call site: The same template literal location always passes the same strings array reference, enabling caching
RunePowered by Rune AI

Frequently Asked Questions

Can I use async tag functions?

Yes. An async tag function returns a Promise. You must `await` the result: `const html = await asyncTag\`...\``.

What happens if the tag function is undefined?

You get a ReferenceError, the same as calling any undefined function.

Can I create a tag that caches results?

Yes. Use a WeakMap keyed on the `strings` array (which is the same reference for identical template literal sites) to cache computed results.

Why does strings always have one more element than values?

Because interpolations sit between string parts. For `A${x}B${y}C`, the string parts are `["A", "B", "C"]` and values are `[x, y]`. There is always one more string part (even if it is empty).

Can tagged templates replace regular expressions for parsing?

For simple patterns, yes. For complex parsing, tagged templates provide cleaner syntax for building DSLs, but they do not replace regex for arbitrary pattern matching.

Conclusion

Tagged template literals are one of JavaScript's most powerful metaprogramming features. They let you intercept template string processing to build HTML sanitizers, SQL builders, CSS-in-JS systems, i18n formatters, and domain-specific languages. The key insight is that static parts and dynamic values arrive as separate arguments, enabling safe-by-default string composition. For the module system used to share tag functions, see JavaScript ES6 modules import export guide. For the spread/rest syntax often used in tag implementations, see JS spread vs rest operator complete tutorial.