2

I am trying to wrap the first letter of each word in my heading tags with a span class so that I can style them using CSS. I have tried to use a snippet I've found on here, but I have 2 h1 tags and it is taking the first one and repeating it for the second!

The function is this:

<script>
  $(document).ready(function() {
    var words = $('h1').text().split(' ');
    var html = '';
    $.each(words, function() {
      html += '<span class="firstLetter">' + this.substring(0, 1) + '</span>' + this.substring(1) + ' ';
      $('h1').html(html);
    });

  });
</script>

So I have an h1 in the banner at the top, and another one at the start of the content, but the function is taking the top banner heading and replacing the content heading with it, but the span class is working!

I know you shouldn't have 2 h1s, but I want to target all headings anyway, and its a CMS for a client so I can't guarantee they won't use multiple h1 going forwards, so I am testing it out!

Nick Parsons
  • 45,728
  • 6
  • 46
  • 64

5 Answers5

6

Recursively loop over the text nodes inside the headings and then wrap the words within a span and replace the text node with a container span that holds all the wrapped words.

Then style the first letter of these spans using the ::first-letter CSS pseudo element.

NOTE:

  1. Directly replacing the innerHTML of the headings might cause bugs if the headings have elements in them.
    For ex: In the snippet below, the headings have some elements within them. The first one has an anchor element and the second one has an svg and these are common use cases. But if you directly replace the innerHTML of these headings that would eliminate these elements inside them, which is not desired. So, it's essential that you only wrap the text nodes within a span.

  2. ::first-letter only works with block-level elements, so you need to set the display property of spans to inline-block.

const headingEls = document.querySelectorAll("h1");

function wrapWithSpan(node) {
  if (node.nodeName === "#text") {
    const containerSpan = document.createElement("span");
    containerSpan.innerHTML = node.textContent
      .split(" ")
      .map((word) => `<span class="word-span">${word}</span>`)
      .join(" ");
    node.parentNode.replaceChild(containerSpan, node);
  } else {
    Array.from(node.childNodes).forEach(wrapWithSpan);
  }
}

headingEls.forEach(wrapWithSpan);
a[href^="#"] {
  display: inline-block;
  text-decoration: none;
  color: #000;
}

a[href^="#"]:hover,
a[href^="#"]:hover *,
a[href^="#"]:focus,
a[href^="#"]:focus * {
  text-decoration: underline;
}

a[href^="#"]:hover::after,
a[href^="#"]:focus::after {
  color: #aaa;
  content: "#";
  margin-left: 0.25rem;
  font-size: 0.75em;
}

h1 .word-span {
  display: inline-block;
}

h1 .word-span::first-letter {
  color: palevioletred;
  text-transform: uppercase;
}
<h1 id="heading"><a href="#heading">Heading with link</a></h1>
<p>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
  in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>

<h1>Heading with SVG <svg height="20" width="20"><circle cx="10" cy="10" r="10" fill="green" /></svg></h1>
<p>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
  in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>

If you just want to style the first letter of each heading, you don't need JS, you can do it by using only the ::first-letter CSS pseudo element.

a[href^="#"] {
  text-decoration: none;
  color: #000;
}

a[href^="#"]:hover,
a[href^="#"]:focus {
  text-decoration: underline;
}

a[href^="#"]:hover::after,
a[href^="#"]:focus::after {
  color: #aaa;
  content: "#";
  margin-left: 0.25rem;
  font-size: 0.75em;
}

h1::first-letter {
  color: palevioletred;
  text-transform: uppercase;
}
<h1 id="heading"><a href="#heading">Heading with link</a></h1>
<p>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
  in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>

<h1>Heading with SVG <svg height="20" width="20"><circle cx="10" cy="10" r="10" fill="green" /></svg></h1>
<p>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
  in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
SSM
  • 2,855
  • 1
  • 3
  • 10
2
const h1 = document.getElementsByTagName("H1");
for (const item of h1) item.innerHTML = item.innerText.split(" ").map(word => "<span style='color: red !important;'>" + word[0] + "</span>" + word.slice(1)).join(" ")

Assuming that the your h1 tags have no children. Its easy to modify this to get the correct result though.

Dimitar
  • 1,148
  • 8
  • 29
1

As per OP's request, this will "wrap the first letter of each word".

Since there are two <h1> elements (as OP said, very wrong), one should iterate them using each too, same way OP did with the words array.

$(document).ready(function() {

    $('h1').each( function(index, heading) {
    
      const words = $(heading).text().split(' ')
      let html = '';
      
      $.each(words, function() {
        html += '<span class="firstLetter">'+this.substring(0,1)+'</span>'+this.substring(1) + ' ';
      })
      
      $(heading).html(html);
    })
    
});
span.firstLetter {
  color: violet;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<h1>Lorem ipsum dolor sit amet</h1>

<hr>

<h1>Bacon ipsum dolor amet biltong pork chop bacon</h1>
GrafiCode
  • 3,307
  • 3
  • 26
  • 31
0

This should work:

.firstLetter
{
color:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1>Hello World</h1>
<h1>Goodbye World</h1>

<script>
        var h1s = document.getElementsByTagName('h1');
        for(const h1 of h1s)
        {
            let text = h1.innerText.split(' ');
            let html = "";
            for(const word of text)
            {
             html += '<span class="firstLetter">' + word.substring(0, 1) + '</span>' + word.substring(1) + ' ';
            }
            h1.innerHTML = html;
        }
        
</script>
anton-tchekov
  • 1,028
  • 8
  • 20
0

To style each first letter of every word in every heading, you can use jQuery's .html() (as you're using jQuery in your question). This will loop over each h1, and replace the text within it with the returned value. The new value replaces the first letter of each word (referred to using $& in the replacement argument) matched using the regular expression \b\w (which matches a word boundary (\b) followed by a character (\w)) with the character itself wrapped in <span> tags:

$("h1").html(function(i, txt) {
  return txt.replace(/\b\w/g, '<span class="firstLetter">$&</span>');
});
.firstLetter {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1>This is a heading</h1>
<h1>This is another heading</h1>
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64