← Text effects

True Focus

A frame of four corner brackets hops from word to word. Whichever word it lands on snaps into sharp focus while the rest of the line stays soft and blurred, like a camera hunting for its subject.

Look closer at every word

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-true-focus to a heading or line of text.

Options

Add any of these attributes to the same element.

data-durationHow long each word stays in focus. Default 1300 ms.
data-blurHow blurred the other words are. Default 5 px.
data-colorColour of the frame. Default #a855f7.
data-glowColour of the soft glow round the frame, or none. Default the frame colour.
data-hoverFocus follows the pointer instead of hopping by itself. On touch screens, tap a word.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // True Focus: a frame of four corner brackets hops from word to word. The
    // framed word snaps into focus while the rest of the line stays blurred.
    // Usage: <h1 data-true-focus>Look closer at this</h1>
    // Optional: data-blur (how blurred the other words are, px; default 5),
    // data-duration (how long each word stays in focus, ms; default 1300),
    // data-color (frame colour; default #a855f7), data-glow (colour of a soft
    // glow round the frame; default the frame colour, "none" for no glow),
    // data-hover (focus follows the pointer instead of hopping on a timer; on
    // touch screens tap a word).
    var DEFAULTS = { blur: 5, duration: 1300, color: '#a855f7' };

    var css =
      '.trf-word{display:inline-block;white-space:nowrap;transition:filter .45s ease,opacity .45s ease}' +
      '.trf-frame{position:absolute;left:0;top:0;pointer-events:none;opacity:0}' +
      '.trf-c{position:absolute;width:.22em;height:.22em;border:0 solid currentColor}' +
      '.trf-tl{left:0;top:0;border-width:.07em 0 0 .07em;border-top-left-radius:.04em}' +
      '.trf-tr{right:0;top:0;border-width:.07em .07em 0 0;border-top-right-radius:.04em}' +
      '.trf-bl{left:0;bottom:0;border-width:0 0 .07em .07em;border-bottom-left-radius:.04em}' +
      '.trf-br{right:0;bottom:0;border-width:0 .07em .07em 0;border-bottom-right-radius:.04em}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function split(el) {
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      var words = [];
      text.split(' ').forEach(function (w, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var s = document.createElement('span');
        s.className = 'trf-word';
        s.setAttribute('aria-hidden', 'true');
        s.textContent = w;
        el.appendChild(s);
        words.push(s);
      });
      return words;
    }

    function setup(el) {
      var d = el.dataset;
      if (reduced()) return;
      var blur = d.blur !== undefined ? +d.blur : DEFAULTS.blur;
      var duration = +d.duration || DEFAULTS.duration;
      var color = d.color || DEFAULTS.color;
      var glow = d.glow || color;
      var hover = d.hover !== undefined && d.hover !== 'false';
      if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
      var words = split(el);

      var frame = document.createElement('span');
      frame.className = 'trf-frame';
      frame.setAttribute('aria-hidden', 'true');
      frame.style.color = color;
      if (glow !== 'none') frame.style.filter = 'drop-shadow(0 0 .12em ' + glow + ')';
      ['tl', 'tr', 'bl', 'br'].forEach(function (c) {
        var s = document.createElement('span');
        s.className = 'trf-c trf-' + c;
        frame.appendChild(s);
      });
      el.appendChild(frame);

      var current = -1;
      var shown = false;
      var move = null;

      // Blur everything except word i, and glide the frame round it.
      function focus(i, instant) {
        current = i;
        words.forEach(function (w, j) {
          w.style.filter = j === i ? 'none' : 'blur(' + blur + 'px)';
          w.style.opacity = j === i ? 1 : 0.55;
        });
        var w = words[i];
        var fs = parseFloat(getComputedStyle(el).fontSize);
        var pad = fs * 0.12;
        var box = {
          x: w.offsetLeft - pad,
          y: w.offsetTop - pad * 0.4,
          width: w.offsetWidth + pad * 2,
          height: w.offsetHeight + pad * 0.8
        };
        if (move) move.pause();
        if (instant || !shown) {
          anime.utils.set(frame, box);
          if (!shown) { shown = true; anime.animate(frame, { opacity: [0, 1], scale: [1.25, 1], duration: 450, ease: 'out(3)' }); }
        } else {
          move = anime.animate(frame, Object.assign({ duration: 520, ease: 'inOut(3)' }, box));
        }
      }

      function next() { focus((current + 1) % words.length); }

      var timer = null;
      var visible = false;
      function schedule() {
        clearTimeout(timer);
        if (visible && !hover) timer = setTimeout(function () { next(); schedule(); }, duration);
      }

      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (visible && current < 0) focus(0);
        schedule();
      }, { threshold: 0.2 }).observe(el);

      // Keep the frame on its word when the text reflows.
      new ResizeObserver(function () { if (current >= 0) focus(current, true); }).observe(el);

      if (hover) {
        words.forEach(function (w, i) {
          w.addEventListener('pointerenter', function () { if (i !== current) focus(i); });
          w.addEventListener('pointerdown', function () { if (i !== current) focus(i); });
        });
      }
    }

    function init() {
      document.querySelectorAll('[data-true-focus]').forEach(function (el) {
        if (el._trf) return;
        el._trf = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-true-focus>Look closer at every word</h1>

<!-- focus the word under the pointer: -->
<h1 data-true-focus data-hover data-blur="3">Pick a word</h1>