How to Format Strings with Tagged Templates
Jan 28, 2026 #JavaScript
ES6 template literals make it easier to construct strings in JavaScript. It’s clean to handle multi-line and one-line strings with variable interpolation as shown in the examples below.
// multi-line
let value = "multiple"
let outputs = `This is a
long long long long long
long long long long long
string which is divided in
${value} lines.`
console.log(outputs)
/*
This is a
long long long long long
long long long long long
string which is divided in
multiple lines.
*/
// one-line
let value = "a single"
let outputs = `This is a \
long long long long long \
long long long long long \
string which is compacted in \
${value} line.`
console.log(outputs)
/*
This is a long long long long long long long long long long string which is compacted in a single line.
*/
Template literals preserve the formatting of templates, this means empty characters will also be included in output strings if the templates have any. This is OK if you mean to do this.
But in some cases, it’s annoying. Especially when your templates are located within nested indentations.
let value = "multiple"
let outputs = `
This is a
long long long long long
long long long long long
string which is divided in
${value} lines.
`
console.log(outputs)
/*
This is a
long long long long long
long long long long long
string which is divided in
multiple lines.
*/
let value = "a single"
let outputs = `\
This is a \
long long long long long \
long long long long long \
string which is compacted in \
${value} line.\
`
console.log(outputs)
/*
This is a long long long long long long long long long long string which is compacted in a single line.
*/
How to Format Template Literals
To format output strings in examples above, you could remove all template indentations which result in ugly code, or better, use regular expressions to trim out unnecessary empty characters, so you don’t need to break readability.
let value = "multiple"
let raw = `
This is a
long long long long long
long long long long long
string which is divided in
${value} lines.
`
// Replace all empty characters in the beginning of each line.
// Remove empty characters in the beginning and the end.
let outputs = raw.replace(/^\s+/gm, "").trim()
console.log(outputs)
/*
This is a
long long long long long
long long long long long
string which is divided in
multiple lines.
*/
let value = "a single"
// `\` is not necessary anymore
let raw = `
This is a
long long long long long
long long long long long
string which is compacted in
${value} line.
`
// Replace all consistent empty characters with a single space.
// Remove empty characters in the beginning and the end.
let outputs = raw.replace(/\s+/g, " ").trim()
console.log(outputs)
/*
This is a long long long long long long long long long long string which is compacted in a single line.
*/
One Step Further
Tagged templates are syntactic sugar used to bind a specific template literal to a “tag” function for parsing it. Processing logic like the above examples can be rewritten using tagged templates with better semantic and readability.
// values are the values of variables in the template
// segments is an array of strings divided by ${} in the template,
// their length is values.length + 1
function multiLine(segments, ...values) {
let raw = segments.reduce((acc, str, i) => acc + str + (values[i] || "") , "")
return raw.replace(/^\s+/gm, "").trim()
}
let value = "multiple"
console.log(multiLine`
This is a
long long long long long
long long long long long
string which is divided in
${value} lines.
`)
/*
This is a
long long long long long
long long long long long
string which is divided in
multiple lines.
*/
// values are the values of variables in the template
// segments is an array of strings divided by ${} in the template,
// its length is values.length + 1
function oneLine(segments, ...values) {
let raw = segments.reduce((acc, str, i) => acc + str + (values[i] || ""), "")
return raw.replace(/\s+/g, " ").trim()
}
let value = "a single"
console.log(oneLine`
This is a
long long long long long
long long long long long
string which is compacted in
${value} line.
`)
/*
This is a long long long long long long long long long long string which is compacted in a single line.
*/
Practical Use Cases
When it comes to real-world scenarios, things won’t be that simple. Templates can be nested and you may need complex logic in “tag” functions. Let’s say you need to build a html library which can nest html snippets and output as a whole.
// Your `html` tag function may start like this
function html(segments, ...values) {
let raw = segments.reduce((acc, str, i) => acc + str + (values[i] || ""), "")
return raw
}
// The usage interface is like the below
let list = html`
<ul>
<li>Tom</li>
<li>Jerry</li>
</ul>
`
let main = html`
<main>
<h1>Good Friends</h1>
${list}
</main>
`
let body = html`
<body>
${main}
</body>
`
console.log(body)
/*
<body>
<main>
<h1>Good Friends</h1>
<ul>
<li>Tom</li>
<li>Jerry</li>
</ul>
</main>
</body>
*/
Obviously, the indentations in the outputs are not tidy, they’re not aligned according to nested levels, and include many unnecessary empty characters. You need to add formatting logic to the html tag function.
// Your new html tag function may be like below
function html(segments, ...values) {
let raw = segments.reduce((acc, str, i) => acc + str + (values[i] || ""), "")
// Find first non-empty line indent size as root level indent size
let rootIndentMatch = raw.match(/^[ \t]*(?=\S)/m)
let rootIndentSize = rootIndentMatch ? rootIndentMatch[0].length : 0
return segments.reduce((acc, str, i) => {
// Remove that rootIndentSize from the beginning of each line
let cleanStr = str.replace(new RegExp(`^[ \\t]{0,${rootIndentSize}}`, "gm"), "")
acc += cleanStr
if (i < values.length) {
// Find the indentation characters after current segment
let currentIndent = (acc.match(/([ \t]*)$/) || ["", ""])[1]
// Prepend the indentations before each value to align it to parent tag
let val = String(values[i]).replace(/\n/g, "\n" + currentIndent)
acc += val
}
return acc
}, "").trim()
}
let list = html`
<ul>
<li>Tom</li>
<li>Jerry</li>
</ul>
`
let main = html`
<main>
<h1>Good Friends</h1>
${list}
</main>
`
let body = html`
<body>
${main}
</body>
`
console.log(body)
/*
<body>
<main>
<h1>Good Friends</h1>
<ul>
<li>Tom</li>
<li>Jerry</li>
</ul>
</main>
</body>
*/
Now, you have a html tag function that has the right logic, and it formats nested html snippets tight and clean.
Other Cases
Several popular open-source projects leverage tagged templates to handle string construction with specific logic. You can explore their source code for deeper insights:.
- Lit A Web Component framework that renders components using tagged templates.
- styled-components A CSS-in-JS library that generates styles from tagged templates.
- Prisma A SQL ORM that uses tagged templates to safely generate raw SQL.
Summary
Template literals are designed to construct long strings, whether multi-line or single-line, while preserving formatting. To maintain code readability, regular expressions can be used to clean up output strings.
Tagged templates further enhance semantic clarity and readability by binding specific functions to templates. This is especially useful for wrapping complex logic and providing a clean, declarative interface.