View the innovative works in action at .
Or by way of , you may obtain the source under the very liberal MIT License.
Cheers --
Jess
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> 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.
Thanks for the info. But I would ask that where it can be used?
We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.