The Listenlight Text Engine

tefflox 0 Tallied Votes 241 Views Share

View the innovative works in action at .

Or by way of , you may obtain the source under the very liberal MIT License.

Cheers --
Jess

<html>
<head>

  <!-- php string $text and function listenlight() defined -->
  
  <!-- javascript variables letter and textstringlength and function textengine() defined -->

</head>
<body>

  <?php  
  
    listenlight($text);
    
   ?>

  <script type="text/javascript">
    
    /* CDATA tag not required here; no html entities present  */
  
    setTimeout("setInterval('textengine();', 90);", 2500);
    
    /**
      *
      *  effectively, the above function call waits 2.5 seconds before calling setInterval()
      *  which calls textengine() on each 90 millisecond interval
      **/
      
   </script>  

</body>
</html>

Dani AI

Generated

Nice demo, — and a good question from about where to use this pattern. The Listenlight-style reveal works best for short, attention-grabbing copy: hero headings, taglines, micro-onboarding steps, small call-to-action lines, or short narrative hooks. Avoid it for long paragraphs, essential instructions, or anything the user might need to find quickly — those should be visible immediately or available to assistive tech.

If you plan to keep or modernize the engine, consider a few practical improvements over the older interval/string-based approach: respect prefers-reduced-motion, start the effect only when the element is visible, split text into grapheme clusters to handle emoji/accents, and use requestAnimationFrame (or a timed delta) for smoother timing. The example below shows a compact, production-ready pattern that includes those ideas and cleans up observers/frames when done.

/* HTML: <div id="listen" data-text="Short headline to reveal"></div> */

(function(){
  const el = document.getElementById('listen');
  if (!el) return;
  const full = el.getAttribute('data-text') || el.textContent.trim();

  if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    el.textContent = full;
    return;
  }

  const segments = (typeof Intl !== 'undefined' && Intl.Segmenter)
    ? Array.from(new Intl.Segmenter(undefined, {granularity:'grapheme'}).segment(full), s => s.segment)
    : Array.from(full);

  el.textContent = '';
  let start;
  const cps = 20; // chars per second
  function tick(ts){
    if (!start) start = ts;
    const shown = Math.min(segments.length, Math.floor(((ts - start)/1000) * cps));
    el.textContent = segments.slice(0, shown).join('');
    if (shown < segments.length) requestAnimationFrame(tick);
  }

  const obs = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      obs.disconnect();
      requestAnimationFrame(tick);
    }
  }, {threshold: 0.2});
  obs.observe(el);
})();

Accessibility and SEO: keep a non-animated, screen-reader-visible copy if the line is important. For decorative text, mark the animation element aria-hidden="true" and put the full text in a visually hidden element. Use textContent not innerHTML to avoid XSS and rendering glitches. Troubleshooting: stuttering usually means the cps is too high for the device, or multiple animations run simultaneously; reduce cps or batch updates. Remember to cancel observers/frames when removing elements to avoid leaks.

This approach keeps the effect pleasant, performant, and usable across devices while answering the original implementation gaps discussed in the thread.

KazmiLahore 0 Newbie Poster

Thanks for the info. But I would ask that where it can be used?

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.