← Typing

Code Line Highlight

A guided tour of a code block. A highlight band glides to the lines for each step while the rest dim, and the explanation for that step appears underneath. It moves on by itself, and the dots below jump straight to a step.

// Fade cards in, one by one
animate('.card', {
  opacity: [0, 1],
  y: [24, 0],
  delay: stagger(80),
  ease: 'outQuart',
});
  1. Target every .card on the page.
  2. Fade each one in as it rises 24px.
  3. Start each card 80 ms after the one before.
  4. Ease out, so they settle gently.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-code-line-highlight to a container holding a <pre> of code and a list of steps. Give each step’s <li> the lines it explains in data-lines.

Options

Add any of these attributes to the same element.

data-linesOn a step: a line number, a range like 3-4, or a list like 1,5.
data-fileA file name for a header bar.
data-intervalTime on each step. Default 2600 ms.
data-loop="false"Stop on the last step instead of starting over.
data-theme="light"A light code block instead of the dark one.

Choosing a step with its dot pauses the tour for a few seconds. Step text is announced politely to screen readers as it changes, and the tour only runs while on screen.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Code Line Highlight: a walkthrough of a code block. A highlight band
    // glides to the lines for each step, the other lines dim, and the
    // explanation for that step appears underneath, stepping through in turn.
    // Usage:
    //   <div data-code-line-highlight>
    //     <pre>const box = document.querySelector('.box');
    //   animate(box, { x: 200 });</pre>
    //     <ol>
    //       <li data-lines="1">Find the element.</li>
    //       <li data-lines="2">Move it 200px to the right.</li>
    //     </ol>
    //   </div>
    // data-lines takes a line number, a range like "2-4", or a list like "1,3".
    // Optional: data-file (file name in the header), data-interval (ms per
    // step, default 2600), data-loop (start over after the last step; on by
    // default, data-loop="false" stops at the end), data-theme="light".
    // The step dots are buttons: picking one jumps there and pauses the tour
    // for a few seconds.
    var DEFAULTS = { interval: 2600 };
    var KEYWORDS = /^(const|let|var|function|return|if|else|for|while|import|from|export|default|new|await|async|class|extends|this|true|false|null|undefined|of|in|typeof)$/;

    var css =
      '.clh{text-align:left;border-radius:12px;overflow:hidden;background:#101015;color:#e4e4e7;box-shadow:0 18px 40px rgba(10,10,20,.28)}' +
      '.clh-light{background:#fff;color:#1f2328;border:1px solid rgba(0,0,0,.08);box-shadow:0 20px 44px -16px rgba(40,10,70,.3)}' +
      '.clh-bar{display:flex;align-items:center;gap:6px;padding:0.6em 0.9em;background:#1b1b22;font-size:0.82em;color:#8a8a93}' +
      '.clh-light .clh-bar{background:#f6f6f8;color:#6b7280}' +
      '.clh-bar i{width:0.75em;height:0.75em;border-radius:50%;background:#ff5f57;flex:none}' +
      '.clh-bar i:nth-child(2){background:#febc2e}.clh-bar i:nth-child(3){background:#28c840}' +
      '.clh-bar span{flex:1;margin-left:0.5em;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
      '.clh-code{position:relative;padding:0.7em 0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.65}' +
      '.clh-band{position:absolute;left:0;right:0;top:0;height:1.65em;background:rgba(192,132,252,.14);border-left:3px solid #c084fc;pointer-events:none}' +
      '.clh-light .clh-band{background:var(--accent-soft,#f3e8ff);border-left-color:var(--accent,#6a0dad)}' +
      '.clh-line{position:relative;display:flex;white-space:pre;transition:opacity .35s}' +
      '.clh-line.clh-dim{opacity:.35}' +
      '.clh-num{flex:none;width:2.6em;padding-right:1em;text-align:right;color:#52525b;user-select:none}' +
      '.clh-src{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;padding-right:1em}' +
      '.clh-k{color:#c084fc}.clh-s{color:#86efac}.clh-n{color:#fbbf24}.clh-c{color:#6b7280;font-style:italic}.clh-f{color:#60a5fa}' +
      '.clh-light .clh-k{color:#7c3aed}.clh-light .clh-s{color:#15803d}.clh-light .clh-n{color:#b45309}.clh-light .clh-f{color:#2563eb}' +
      '.clh-foot{display:flex;align-items:flex-start;gap:0.8em;padding:0.8em 1em 0.9em;border-top:1px solid rgba(127,127,140,.22)}' +
      '.clh-step{flex:none;display:grid;place-items:center;width:1.7em;height:1.7em;border-radius:50%;background:#c084fc;color:#101015;font-weight:700;font-size:0.85em}' +
      '.clh-light .clh-step{background:var(--accent,#6a0dad);color:#fff}' +
      '.clh-notes{flex:1;min-width:0;display:grid;line-height:1.45}' +
      '.clh-note{grid-area:1/1;margin:0;visibility:hidden}' +
      '.clh-note.clh-on{visibility:visible}' +
      '.clh-dots{display:flex;gap:0.35em;padding:0 1em 0.9em}' +
      '.clh-dots button{width:1.6em;height:0.4em;padding:0;border:0;border-radius:99px;background:rgba(127,127,140,.35);cursor:pointer;transition:background-color .3s,width .3s}' +
      '.clh-dots button[aria-current]{width:2.6em;background:#c084fc}' +
      '.clh-light .clh-dots button[aria-current]{background:var(--accent,#6a0dad)}' +
      '.clh-dots span{width:1.6em;height:0.4em;border-radius:99px;background:rgba(127,127,140,.35);transition:background-color .3s,width .3s}' +
      '.clh-dots span.clh-cur{width:2.6em;background:#c084fc}' +
      '.clh-light .clh-dots span.clh-cur{background:var(--accent,#6a0dad)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function opt(d, k) { return d[k] !== undefined && d[k] !== '' ? d[k] : DEFAULTS[k]; }
    function esc(t) { return t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

    // A small JavaScript-style colourer: comments, strings, numbers, keywords, calls.
    function colour(line) {
      return line.replace(/(\/\/.*$)|('[^']*'?|"[^"]*"?|`[^`]*`?)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)(\s*\()?|([^'"`\/\w$]+|\/)/g, function (m, c, s, n, w, call, rest) {
        if (c) return '<span class="clh-c">' + esc(c) + '</span>';
        if (s) return '<span class="clh-s">' + esc(s) + '</span>';
        if (n) return '<span class="clh-n">' + n + '</span>';
        if (w) {
          if (KEYWORDS.test(w)) return '<span class="clh-k">' + w + '</span>' + (call ? esc(call) : '');
          return call ? '<span class="clh-f">' + w + '</span>' + esc(call) : w;
        }
        return esc(rest || m);
      });
    }

    function parseLines(spec, max) {
      var out = [];
      String(spec || '').split(',').forEach(function (part) {
        var m = part.trim().match(/^(\d+)(?:\s*-\s*(\d+))?$/);
        if (!m) return;
        var a = +m[1], b = m[2] ? +m[2] : a;
        for (var i = Math.max(1, a); i <= Math.min(max, b); i++) out.push(i);
      });
      return out;
    }

    function setup(el) {
      var d = el.dataset;
      var pre = el.querySelector('pre');
      var lines = (pre ? pre.textContent : '').replace(/\r/g, '').replace(/^\n+|\s+$/g, '').split('\n');
      var steps = Array.from(el.querySelectorAll('li')).map(function (li) {
        return { html: li.innerHTML, lines: parseLines(li.getAttribute('data-lines'), lines.length) };
      });
      var thumb = !!el.closest('[aria-hidden="true"]') || !!el.closest('.anim-thumb');
      el.textContent = '';
      el.classList.add('clh');
      if (d.theme === 'light') el.classList.add('clh-light');
      var bar = document.createElement('div');
      bar.className = 'clh-bar';
      bar.setAttribute('aria-hidden', 'true');
      bar.innerHTML = '<i></i><i></i><i></i><span></span>';
      bar.lastChild.textContent = d.file || '';
      var code = document.createElement('pre');
      code.className = 'clh-code';
      code.style.margin = '0';
      code.innerHTML = '<span class="clh-band" aria-hidden="true"></span>' + lines.map(function (l, i) {
        return '<span class="clh-line"><span class="clh-num" aria-hidden="true">' + (i + 1) + '</span><span class="clh-src">' + (colour(l) || ' ') + '</span></span>';
      }).join('');
      var foot = document.createElement('div');
      foot.className = 'clh-foot';
      foot.innerHTML = '<span class="clh-step" aria-hidden="true">1</span><div class="clh-notes" aria-live="polite"></div>';
      var notes = foot.lastChild;
      steps.forEach(function (st) {
        var p = document.createElement('p');
        p.className = 'clh-note';
        p.innerHTML = st.html;
        notes.appendChild(p);
        st.note = p;
      });
      var dots = document.createElement('div');
      dots.className = 'clh-dots';
      steps.forEach(function (st, i) {
        var b;
        if (thumb) {
          b = document.createElement('span');
        } else {
          b = document.createElement('button');
          b.type = 'button';
          b.setAttribute('aria-label', 'Step ' + (i + 1));
        }
        dots.appendChild(b);
        st.dot = b;
      });
      if (d.file) el.appendChild(bar);
      el.appendChild(code);
      el.appendChild(foot);
      if (steps.length > 1) el.appendChild(dots);
      var s = {
        el: el, d: d, steps: steps, band: code.firstChild, lines: Array.from(code.querySelectorAll('.clh-line')),
        num: foot.firstChild, i: -1, timer: 0, visible: false, pausedUntil: 0
      };
      steps.forEach(function (st, i) {
        if (st.dot.tagName === 'BUTTON') st.dot.addEventListener('click', function () {
          s.pausedUntil = Date.now() + 6000;
          go(s, i, false);
        });
      });
      return s;
    }

    function go(s, i, instant) {
      var st = s.steps[i];
      if (!st) return;
      var prev = s.steps[s.i];
      s.i = i;
      var on = {};
      st.lines.forEach(function (n) { on[n] = true; });
      s.lines.forEach(function (l, j) { l.classList.toggle('clh-dim', st.lines.length > 0 && !on[j + 1]); });
      s.steps.forEach(function (x, j) {
        x.note.classList.toggle('clh-on', j === i);
        if (x.dot.tagName === 'BUTTON') {
          if (j === i) x.dot.setAttribute('aria-current', 'step'); else x.dot.removeAttribute('aria-current');
        } else x.dot.classList.toggle('clh-cur', j === i);
      });
      s.num.textContent = i + 1;
      // The band covers the first to the last highlighted line.
      var first = s.lines[(st.lines[0] || 1) - 1], last = s.lines[(st.lines[st.lines.length - 1] || 1) - 1];
      var top = first.offsetTop, h = last.offsetTop + last.offsetHeight - top;
      var show = st.lines.length ? 1 : 0;
      if (instant) {
        anime.utils.set(s.band, { y: top, height: h, opacity: show });
        return;
      }
      anime.animate(s.band, { y: top, height: h, opacity: show, duration: 520, ease: 'inOut(3)' });
      anime.animate(st.note, { opacity: [0, 1], x: [10, 0], duration: 420, ease: 'out(3)' });
      if (prev !== st) anime.animate(s.num, { scale: [0.6, 1], duration: 380, ease: 'outBack(2)' });
    }

    function tick(s) {
      if (!s.visible || Date.now() < s.pausedUntil) return;
      var next = s.i + 1;
      if (next >= s.steps.length) {
        if (s.d.loop === 'false') { stop(s); return; }
        next = 0;
      }
      go(s, next, false);
    }

    function start(s) {
      if (s.timer) return;
      s.timer = setInterval(function () { tick(s); }, +opt(s.d, 'interval'));
    }

    function stop(s) {
      clearInterval(s.timer);
      s.timer = 0;
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._clh;
          s.visible = e.isIntersecting;
          if (s.visible) {
            if (s.i < 0) go(s, 0, false);
            if (!(s.d.loop === 'false' && s.i >= s.steps.length - 1)) start(s);
          } else stop(s);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-code-line-highlight]').forEach(function (el) {
        if (el._clh) return;
        var s = (el._clh = setup(el));
        go(s, 0, true);
        s.i = -1;
        if (window.ResizeObserver) new ResizeObserver(function () { if (s.i >= 0) go(s, s.i, true); }).observe(el);
        if (reduced()) {
          // No gliding or auto-advance: step 1 shows, the dots still switch steps.
          s.i = 0;
          return;
        }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-code-line-highlight>
  <pre>const box = $('.box');
animate(box, { x: 240 });</pre>
  <ol>
    <li data-lines="1">Find the box.</li>
    <li data-lines="2">Slide it 240px right.</li>
  </ol>
</div>