Sometimes when using CSS, we need certain DOM elements to exist to apply styles properly. An example is the use of text nodes and spacing. If we want to put space between text blocks, they must be wrapped in a span to apply margins properly.

In some cases where the content is out of your control, such as a CMS system, you may need to find and wrap these text nodes to style them properly. Let’s take a look at an example.

<section>
  <h1>A heading</h1>

  <p>a paragraph</p>

  some text

  <hr>

  some text
</section>

Our HTML here has two unwrapped text nodes, some text. A next node contains all text characters between two HTML elements, including whitespace. If my system needed to add margins between these elements, it would unfortunately not work.

To solve this, we need to query the child elements, find all text nodes with characters, and then wrap them with span elements. Our ideal output would be like the following:

<section>
  <h1>A heading</h1>

  <p>a paragraph</p>

  <span>some text</span>

  <hr>

  <span>some text</span>
</section>

To find and wrap all our text nodes, we must be careful to preserve the text node references. Instead of making a span and copying the text, we must move the text node into a newly created span. This is important as the text could be a text node that is being used somewhere else. Let’s look at this example:


<section>
  <h1>A heading</h1>

  <p>a paragraph</p>

  {{textBinding}}

  <hr>

  {{anotherTextBinding}}

</section>

Here we have a template using some framework template binding. This binding may update the text value over time. If we copy the text into a new span deleting the old text, we will break the text binding from being updated in the future.

To safely move our text node into a span element, we need to find all the text nodes we care about. This can vary slightly, but we want any text node with characters and no empty nodes in our use case.

const textNodes = getAllTextNodes(document.querySelector('section'));

function getAllTextNodes(element) {
  return Array.from(element.childNodes)
    .filter(node => node.nodeType === 3 && node.textContent.trim().length > 1);
}

With this function, given an HTML element, we can find all child nodes, which are nodeType value of 3 (text) and have at least one character in the node.

Now that we can get a list of text nodes, we can start moving them into new span elements.

textNodes.forEach(node => {
  const span = document.createElement('span');
  node.after(span);
  span.appendChild(node);
});

We iterate through each text node and create a span element appending it after the next node. Once the span is added, we use the existing text node and append it as a child of the span element. This allows us to preserve the text node without breaking any references. To test this, we can use a setInterval to change the text node value ever second.

const textNodes = Array.from(document.querySelector('section').childNodes)
  .filter(node => node.nodeType === 3 && node.textContent.trim().length > 1)

textNodes.forEach(node => {
  const span = document.createElement('span');
  node.after(span);
  span.appendChild(node);
});

setInterval(() =>
  textNodes.forEach(node => node.textContent = Math.random())
, 1000);

We can see how we can continue to refer to the text nodes even after moving them into our span wrappers. An alternative technique, if you want to add only space, is to use the CSS Flex Gap and Grid Gap properties, which will add space between elements, including text nodes.

Check out the full working demo below with the wrapper logic and CSS Gap alternative!

Web Component Essentials

Build reusable UI components with my new Course & E-Book!

Start now!

View Demo Code