JavaScript
text manipulation
DOM manipulation
HTML
web development

How to wrap part of a text in a node with JavaScript

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Wrapping only part of a text node is a common DOM task when you need to highlight a substring, attach a tooltip, or turn one word into a link without rebuilding the entire element. The tricky part is that text inside the DOM is not automatically split into convenient chunks, so you need to create that structure yourself.

Why Partial Wrapping Is Different From Replacing innerHTML

The fastest-looking approach is often element.innerHTML = ..., but that is usually the wrong tool. Replacing HTML as a string destroys existing child nodes, removes event listeners attached below that element, and forces you to manually escape text.

If you only want to wrap a substring inside existing text, work with real text nodes instead. That preserves the rest of the DOM and makes the change more predictable.

For example, imagine this HTML:

html
<p id="message">JavaScript can wrap only one word here.</p>

You want to wrap the word only in a span so it can be styled.

A Simple and Reliable Approach With splitText

If you already know the target text node and the character offsets, splitText is the cleanest solution. It breaks one text node into smaller text nodes, which lets you insert a wrapper element around the middle part.

javascript
1const paragraph = document.getElementById("message");
2const textNode = paragraph.firstChild;
3const target = "only";
4const start = textNode.data.indexOf(target);
5
6if (start !== -1) {
7  const middle = textNode.splitText(start);
8  const after = middle.splitText(target.length);
9
10  const wrapper = document.createElement("span");
11  wrapper.className = "highlight";
12  wrapper.textContent = middle.data;
13
14  paragraph.replaceChild(wrapper, middle);
15  paragraph.insertBefore(after, wrapper.nextSibling);
16}

Why this works:

  • 'splitText(start) separates the leading text from the target and everything after it.'
  • A second splitText(target.length) isolates the exact substring.
  • You replace just that isolated node with a real element.

This pattern is good when the content is mostly plain text and the element contains one direct text node.

Using Range for More Complex DOM Content

If the target text is mixed with other inline nodes, Range is often safer. A range can select a slice of a text node and wrap it without rebuilding surrounding markup.

html
<p id="note">A fast fox jumps over the fence.</p>
javascript
1const note = document.getElementById("note");
2const textNode = note.firstChild;
3const phrase = "fox";
4const start = textNode.data.indexOf(phrase);
5
6if (start !== -1) {
7  const range = document.createRange();
8  range.setStart(textNode, start);
9  range.setEnd(textNode, start + phrase.length);
10
11  const mark = document.createElement("mark");
12  range.surroundContents(mark);
13}

Range.surroundContents is concise, but it has one important limitation: the selected range must be structurally valid. If the selection crosses incompatible node boundaries, the browser throws an error. When you are not sure the selection is contained neatly in one text node, split the text first or process smaller nodes individually.

Finding the Correct Text Node

Real pages often contain multiple nested text nodes. In that case, firstChild is not enough. You may need to scan text nodes with a TreeWalker.

javascript
1function findTextNode(root, searchText) {
2  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
3
4  while (walker.nextNode()) {
5    const node = walker.currentNode;
6    const index = node.data.indexOf(searchText);
7    if (index !== -1) {
8      return [node, index];
9    }
10  }
11
12  return null;
13}
14
15const container = document.getElementById("message");
16const result = findTextNode(container, "wrap");
17
18if (result) {
19  const [node, index] = result;
20  const middle = node.splitText(index);
21  middle.splitText("wrap".length);
22
23  const strong = document.createElement("strong");
24  strong.textContent = middle.data;
25  middle.parentNode.replaceChild(strong, middle);
26}

This is a better foundation when the element contains nested formatting or dynamic content.

When You Need All Matches, Not Just One

A common next step is wrapping every occurrence of a word. That is still easier with DOM traversal than with string-based HTML replacement. Process text nodes one by one, and always restart carefully after each split, because splitting changes node boundaries.

For large documents, avoid rescanning the entire subtree after every replacement. Instead, collect candidate text nodes first, then transform them in order.

Common Pitfalls

  • Replacing innerHTML just to wrap one substring, which removes existing listeners and can introduce escaping bugs.
  • Assuming element.firstChild is always the text node you want.
  • Using Range.surroundContents across mixed nodes and getting runtime exceptions.
  • Forgetting that splitText mutates the original node structure.
  • Searching raw text without deciding whether you want the first match, all matches, or only whole-word matches.

Summary

  • Use DOM node operations instead of rewriting HTML strings.
  • 'splitText is the simplest option when you know the exact text node and offsets.'
  • 'Range is concise, but only works when the selection is structurally valid.'
  • Use TreeWalker when the text may be nested inside several child nodes.
  • Decide early whether your wrapper logic targets one match or every match in the subtree.

Course illustration
Course illustration

All Rights Reserved.