Search code examples
javascripthtmlcss

Add Caret to Typed Text


I have some text that types a message. I have seen this done many different ways, but I like how simplistic this approach is.

The thing it doesn't have is a caret at the end of each line, I tried to add one myself via css, but the caret gets aligned to the far right of each line instead of stopping at the end of the text. Also, the caret should only show on the line it's on. The one I created shows, but then each new line also gets a caret, so by the end I'm left with this big giant cursor which works...but definitely not what I had anticipated.

I thought about adding display: inline to the <p> tags, but I want each new p tag to be on a new line.

const paragraphs = document.querySelectorAll('#str p'); // Select all <p> tags within #str
let delay = 0;

paragraphs.forEach(p => {
  const text = p.textContent;
  p.textContent = ''; // Clear the text content initially

  let index = 0;

  function typeWriter() {
    if (index < text.length) {
      p.textContent += text.charAt(index);
      index++;
      setTimeout(typeWriter, 90); // Adjust typing speed here
    }
  }

  setTimeout(typeWriter, delay); // Delay between paragraphs
  delay += text.length * 90; // Increase delay based on text length
});
#str {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 20px;
  height: auto;
  font-family: sans-serif;
  font-size: 40px;
  margin: 20px auto;
  position: relative;
  color: #fff;
  font-weight: bold;
}

#str p {
  border-right: .15em solid orange;
  overflow: hidden;
  position: relative;
  animation: blink-caret .5s step-end infinite;
}

@keyframes blink-caret {
  from,
  to {
    border-color: transparent
  }
  50% {
    border-color: orange
  }
}
<div id="str">
  <p>Something Cool</p>
  <p>Shows</p>
  <p>Up In here!</p>
</div>


Solution

  • Here is a solution with a couple modifications:

    Instead of styling the paragraphs, you can use an :after pseudo element with display: inline-block. Combined with other conditions, you could use this selector:

    #str p:not(:empty):not(.complete:not(:last-child)):after
    
    • :not(:empty) will prevent the caret from showing when the paragraph has not started yet
    • :not(.complete:not(:last-child)) will prevent the caret from showing when the paragraph is complete, except if it's the last one

    In your JS, you can add the complete class to the paragraph once it's complete:

    p.classList.add('complete');
    

    const paragraphs = document.querySelectorAll('#str p'); // Select all <p> tags within #str
    let delay = 0;
    
    paragraphs.forEach(p => {
      const text = p.textContent;
      p.textContent = ''; // Clear the text content initially
    
      let index = 0;
    
      function typeWriter() {
        if (index < text.length) {
          p.textContent += text.charAt(index);
          index++;
          setTimeout(typeWriter, 90); // Adjust typing speed here
        } else {
          p.classList.add('complete');
        }
      }
    
      setTimeout(typeWriter, delay); // Delay between paragraphs
      delay += text.length * 90; // Increase delay based on text length
    });
    #str {
      padding: 5px;
      font-family: sans-serif;
      font-size: 15px;
      margin: 5px auto;
      position: relative;
      color: #000;
      font-weight: bold;
    }
    
    #str p:not(:empty):not(.complete:not(:last-child)):after {
      content: "";
      display: inline-block;
      height: 1em;
      border-right: .15em solid orange;
      position: relative;
      animation: blink-caret 0.5s step-end infinite;
    }
    
    @keyframes blink-caret {
      from,
      to {
        border-color: transparent
      }
      50% {
        border-color: orange
      }
    }
    <div id="str">
      <p>Something Cool</p>
      <p>Shows</p>
      <p>Up In here!</p>
    </div>