Creating Multi-Line Strings in JS With Backticks
Learn every technique for creating multi-line strings in JavaScript using template literals. Master backtick syntax, indentation handling, HTML templates, and whitespace management with practical examples.
Before ES2015, creating multi-line strings in JavaScript was awkward. You had to escape newlines with backslashes, concatenate strings with +, or join arrays. Template literal (backtick strings) solved this by letting you write strings that span multiple lines naturally, with the line breaks preserved exactly as written. This guide covers every multi-line string technique with a focus on template literals, including how to handle the indentation and whitespace issues that come with them.
If you are looking for a broader overview of template literals and strings, that guide covers interpolation, tagged templates, and string methods. This article focuses specifically on multi-line patterns.
The Old Way: Before Template Literals
Before ES2015, developers used three workarounds for multi-line strings, each with drawbacks:
String Concatenation
const html = "<div>\n" +
" <h1>Welcome</h1>\n" +
" <p>Hello, World!</p>\n" +
"</div>";This works but is tedious and error-prone. Every line needs quotes, a + operator, and an explicit \n.
Backslash Line Continuation
const html = "<div>\n\
<h1>Welcome</h1>\n\
<p>Hello, World!</p>\n\
</div>";The backslash \ at the end of each line tells JavaScript to continue on the next line. However, it does not insert a newline character. You still need \n for actual line breaks. Adding a space after the \ breaks the continuation silently.
Array Join
const html = [
"<div>",
" <h1>Welcome</h1>",
" <p>Hello, World!</p>",
"</div>",
].join("\n");This is the cleanest pre-ES6 approach, but it still requires wrapping every line in quotes and managing the array structure.
| Method | Readability | Error-Prone | Preserves Indentation |
|---|---|---|---|
Concatenation (+) | Poor | Yes (missing \n) | Manual |
Backslash (\) | Poor | Yes (trailing spaces) | No |
Array .join() | Moderate | Moderate | Manual |
Template literals (`) | Excellent | No | Automatic |
Template Literals: The Modern Way
Template literals preserve newlines exactly as written. No escape sequences, no concatenation, no arrays:
const html = `<div>
<h1>Welcome</h1>
<p>Hello, World!</p>
</div>`;
console.log(html);
// <div>
// <h1>Welcome</h1>
// <p>Hello, World!</p>
// </div>The string contains the exact whitespace and line breaks you see in the source code. This is both the feature and the challenge of template literals.
Practical Multi-Line Patterns
HTML Templates
Template literals excel at building HTML strings:
function createCard(user) {
return `
<article class="user-card">
<h2>${user.name}</h2>
<p class="role">${user.role}</p>
<p class="email">${user.email}</p>
<span class="status ${user.active ? "active" : "inactive"}">
${user.active ? "Active" : "Inactive"}
</span>
</article>
`.trim();
}
const card = createCard({
name: "Ada Lovelace",
role: "Engineer",
email: "ada@runehub.dev",
active: true,
});Using .trim() at the end removes the leading and trailing newlines created by placing the backtick on its own line.
SQL Queries
const query = `
SELECT
users.id,
users.name,
users.email,
COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
WHERE users.active = true
GROUP BY users.id
ORDER BY order_count DESC
LIMIT 10
`.trim();Error Messages
function validateAge(age) {
if (typeof age !== "number" || age < 0 || age > 150) {
throw new Error(`
Invalid age value: ${age}
Expected: a number between 0 and 150
Received: ${typeof age} with value ${JSON.stringify(age)}
Check the input source and ensure proper type conversion.
`.trim());
}
}Configuration Strings
function generateNginxConfig(domain, port) {
return `
server {
listen 80;
server_name ${domain};
location / {
proxy_pass http://localhost:${port};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
`.trim();
}The Indentation Problem
Template literals preserve ALL whitespace, including the indentation from your source code. This causes issues when the template is nested inside functions or conditionals:
function getWelcomeMessage(name) {
if (name) {
return `
Welcome, ${name}!
Here are your options:
- View dashboard
- Edit profile
- Sign out
`;
}
}
console.log(getWelcomeMessage("Ada"));
// Output has 6 spaces of unwanted indentation on every line:
//
// Welcome, Ada!
//
// Here are your options:
// - View dashboard
// - Edit profile
// - Sign out
// The string contains the indentation from the source code because template literals do not strip leading whitespace.
Solution 1: Align to the Left Margin
function getWelcomeMessage(name) {
if (name) {
return `Welcome, ${name}!
Here are your options:
- View dashboard
- Edit profile
- Sign out`;
}
}This produces correct output but makes the source code ugly when the template is deeply nested.
Solution 2: Use .trim() and .replace()
function getWelcomeMessage(name) {
if (name) {
return `
Welcome, ${name}!
Here are your options:
- View dashboard
- Edit profile
- Sign out
`.replace(/^ {6}/gm, "").trim();
}
}The regex ^ {6} matches exactly 6 leading spaces on each line (the unwanted indentation) and removes them.
Solution 3: Dedent Utility Function
A reusable dedent function strips the common leading whitespace from all lines:
function dedent(strings, ...values) {
// Build the full string
let result = strings.reduce((acc, str, i) => {
return acc + str + (values[i] !== undefined ? values[i] : "");
}, "");
// Find the minimum indentation (ignoring empty lines)
const lines = result.split("\n");
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
const minIndent = nonEmptyLines.reduce((min, line) => {
const indent = line.match(/^(\s*)/)[1].length;
return Math.min(min, indent);
}, Infinity);
// Remove the common indentation and trim
return lines
.map((line) => line.slice(minIndent))
.join("\n")
.trim();
}
// Usage
function getWelcomeMessage(name) {
return dedent`
Welcome, ${name}!
Here are your options:
- View dashboard
- Edit profile
- Sign out
`;
}
console.log(getWelcomeMessage("Ada"));
// Welcome, Ada!
//
// Here are your options:
// - View dashboard
// - Edit profile
// - Sign outDedent Libraries
For production use, the npm package dedent (or ts-dedent for TypeScript) provides a well-tested tagged template function that handles edge cases like tabs, mixed indentation, and expression alignment.
Escape Characters in Multi-Line Strings
Even with template literals, you sometimes need escape characters:
// Escaping backticks inside template literals
const code = `Use \`backticks\` for template literals.`;
// Escaping ${} to show literal syntax
const docs = `Use \${variable} syntax for interpolation.`;
// Mixing escape sequences with literal newlines
const mixed = `Line 1
Line 2\tTabbed content
Line 3`;| Escape Sequence | Output | Works In Template Literals? |
|---|---|---|
\n | Newline | Yes (but usually unnecessary) |
\t | Tab | Yes |
\\ | Backslash | Yes |
\` | Backtick | Yes (required inside template literals) |
\${ | Literal ${ | Yes (prevents interpolation) |
Building Dynamic Multi-Line Content
Lists from Arrays
const features = ["Fast compilation", "Type safety", "Hot reload", "Built-in routing"];
const featureList = `
Key Features:
${features.map((f, i) => ` ${i + 1}. ${f}`).join("\n")}
`.trim();
console.log(featureList);
// Key Features:
// 1. Fast compilation
// 2. Type safety
// 3. Hot reload
// 4. Built-in routingConditional Sections
function generateEmail(user, order) {
return `
Hi ${user.name},
Thank you for your order #${order.id}.
${order.items.map((item) => `- ${item.name}: $${item.price.toFixed(2)}`).join("\n")}
Total: $${order.total.toFixed(2)}
${order.discount ? `\nDiscount applied: -$${order.discount.toFixed(2)}` : ""}
${order.notes ? `\nNotes: ${order.notes}` : ""}
Best regards,
RuneHub Team
`.trim();
}Best Practices
Multi-Line String Guidelines
Follow these practices for clean, maintainable multi-line strings.
Always call .trim() on multi-line template results. Starting and ending a template on their own lines creates leading/trailing newlines. .trim() removes them cleanly.
Use a dedent function for deeply nested templates. When templates appear inside functions, conditionals, or classes, a dedent utility keeps the source code readable while producing correctly indented output.
Keep template literals readable in source code. If a template is longer than 15-20 lines, consider moving it to a separate function or file. Long inline templates make the containing function hard to read.
Use .join() for dynamic line generation. When building lists from arrays, array.map().join("\n") is more readable and maintainable than manual string concatenation.
Test multi-line output explicitly. Whitespace bugs are invisible in console.log(). Use assertions that check for exact string content, including indentation.
Common Mistakes and How to Avoid Them
Multi-Line Pitfalls
Template literal whitespace issues are among the most common formatting bugs in JavaScript applications.
Including source indentation in the output. The most common template literal mistake. Always test the actual output string, not just how it looks in the console. Use .trim() and dedent patterns.
Forgetting that empty lines have whitespace. An "empty" line inside a template literal may contain spaces from your source indentation. This can break text processing that checks for truly empty lines with line === "".
Using \n when a literal newline works. Inside template literals, just press Enter for a new line. Using \n inside backtick strings adds unnecessary noise.
Mixing tabs and spaces in templates. If your editor uses tabs for indentation but the template literal needs spaces (or vice versa), the output will have inconsistent whitespace. Configure your editor to use consistent whitespace.
Next Steps
Master template literal interpolation
Explore advanced interpolation patterns including nested templates, conditional expressions, and function calls inside ${}.
Learn tagged templates
Build custom tagged template functions for SQL sanitization, HTML escaping, and internationalization.
Explore [JavaScript operators](/tutorials/programming-languages/javascript/js-operators-arithmetic-logical-comparison)
Understand how string operators like + and comparison operators interact with template literal output.
Build an email template system
Create a reusable email generator using template literals with conditional sections, dynamic lists, and the dedent pattern for clean formatting.
Rune AI
Key Insights
- Backticks preserve line breaks: template literals include newlines exactly as written in source code
- Always use .trim(): multi-line templates create leading/trailing whitespace that needs removal
- Dedent for nested templates: a dedent utility strips unwanted source indentation from output
- Use .map().join() for dynamic lists: this pattern is cleaner and more maintainable than loop concatenation
- Whitespace is invisible: always test multi-line output explicitly, not just visual console output
Frequently Asked Questions
How do I create a multi-line string in JavaScript?
Do template literals preserve whitespace and indentation?
What is the dedent pattern for template literals?
Can I use variables inside multi-line template literals?
How do I escape backticks inside a template literal?
Conclusion
Template literals made multi-line strings a first-class feature in JavaScript, replacing the error-prone concatenation and backslash approaches of the past. The key to using them well is managing whitespace: call .trim() on results, use a dedent utility for nested templates, and build dynamic content with .map().join("\n") instead of manual string concatenation. These patterns produce clean output while keeping your source code readable and maintainable.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.