← Broadcast

Title Card

A bold title card that builds itself for a video intro. Corner brackets snap out, a rule draws outwards from the centre, the kicker spreads in, the title’s words rise from behind the line, a second rule draws underneath and the subtitle fades up.

Episode 04

The Long Way Home

A film by Ava Moreno

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-title-card to a box holding a heading. Anything before the heading becomes the small kicker line, anything after it the subtitle.
  3. Size it with font-size; the title is 2.6 times that.

Options

Add any of these attributes to the same element.

data-colorColour of the lines and brackets. Default: the text colour.
data-loopTake itself apart and build again.
data-holdHow long it stays built before looping. Default 2600 ms.
data-corners="false"Leave out the corner brackets.
data-delayWait before it starts building. Default 200 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Title Card: a bold title card that builds itself, for video intros.
    // Corner brackets snap out, a rule draws outwards from the centre, the
    // small kicker line spreads in, the title's words rise from behind the
    // rule, a second rule draws under it and the subtitle fades up. Loop it and
    // it takes itself apart again before rebuilding.
    // Usage: <div data-title-card>
    //          <small>Episode 04</small>
    //          <h2>The Long Way Home</h2>
    //          <p>A film by Ava Moreno</p>
    //        </div>
    // Everything before the heading is the kicker, everything after it the
    // subtitle; both are optional. Size it with font-size.
    // Optional: data-color (lines and brackets, default the text colour),
    // data-loop (take it apart and build it again), data-hold (ms it stays
    // built before looping, default 2600), data-corners="false" (no brackets),
    // data-delay (ms before it starts, default 200).
    var DEFAULTS = { hold: 2600, delay: 200 };

    var css =
      '.ttc{display:inline-block;text-align:center}' +
      '.ttc-in{position:relative;display:flex;flex-direction:column;align-items:center;padding:1.1em 1.6em;--ttc-c:currentColor}' +
      '.ttc-in>*{margin:0}' +
      '.ttc-kick{display:block;text-transform:uppercase;letter-spacing:0.3em;font-size:0.8em;font-weight:600;opacity:.8;margin-bottom:0.7em;padding-left:0.3em}' +
      '.ttc-head{font-size:2.6em;line-height:1.02;font-weight:800;letter-spacing:-0.03em;margin:0.18em 0 0.22em}' +
      '.ttc-w{display:inline-block;overflow:hidden;vertical-align:top;padding:0.06em 0.04em 0.1em;margin:-0.06em -0.04em -0.1em}' +
      '.ttc-wi{display:inline-block}' +
      '.ttc-sub{display:block;margin-top:0.8em;font-size:0.95em;opacity:.8}' +
      '.ttc-rule{display:block;width:100%;height:0.12em;min-height:2px;background:var(--ttc-c);border-radius:1px}' +
      '.ttc-corner{position:absolute;width:1em;height:1em;border:0 solid var(--ttc-c)}' +
      '.ttc-tl{left:0;top:0;border-width:0.14em 0 0 0.14em;transform-origin:0 0}' +
      '.ttc-tr{right:0;top:0;border-width:0.14em 0.14em 0 0;transform-origin:100% 0}' +
      '.ttc-bl{left:0;bottom:0;border-width:0 0 0.14em 0.14em;transform-origin:0 100%}' +
      '.ttc-br{right:0;bottom:0;border-width:0 0.14em 0.14em 0;transform-origin:100% 100%}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      var kids = Array.prototype.slice.call(el.children);
      var hi = -1;
      kids.forEach(function (k, i) { if (hi < 0 && /^H[1-6]$/.test(k.tagName)) hi = i; });
      if (hi < 0) hi = kids.length > 1 ? 1 : 0;
      var head = kids[hi];
      var before = kids.slice(0, hi), after = kids.slice(hi + 1);

      el.classList.add('ttc');
      var inner = document.createElement('div');
      inner.className = 'ttc-in';
      if (d.color) inner.style.setProperty('--ttc-c', d.color);

      // The title's words, each behind its own little mask.
      var text = head ? head.textContent.replace(/\s+/g, ' ').trim() : '';
      var words = [];
      if (head) {
        head.classList.add('ttc-head');
        head.setAttribute('aria-label', text);
        head.textContent = '';
        text.split(' ').forEach(function (w, i) {
          if (i) head.appendChild(document.createTextNode(' '));
          var o = document.createElement('span');
          o.className = 'ttc-w';
          o.setAttribute('aria-hidden', 'true');
          var wi = document.createElement('span');
          wi.className = 'ttc-wi';
          wi.textContent = w;
          o.appendChild(wi);
          head.appendChild(o);
          words.push(wi);
        });
      }

      function rule() {
        var r = document.createElement('span');
        r.className = 'ttc-rule';
        r.setAttribute('aria-hidden', 'true');
        return r;
      }
      var top = rule(), bottom = rule();
      before.forEach(function (k) { k.classList.add('ttc-kick'); inner.appendChild(k); });
      inner.appendChild(top);
      if (head) inner.appendChild(head);
      inner.appendChild(bottom);
      after.forEach(function (k) { k.classList.add('ttc-sub'); inner.appendChild(k); });

      var corners = [];
      if (d.corners !== 'false') {
        ['tl', 'tr', 'bl', 'br'].forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'ttc-corner ttc-' + c;
          s.setAttribute('aria-hidden', 'true');
          inner.appendChild(s);
          corners.push(s);
        });
      }
      el.textContent = '';
      el.appendChild(inner);
      el._ttc = { corners: corners, top: top, bottom: bottom, words: words, before: before, after: after };
      if (!reduced()) reset(el);
    }

    function reset(el) {
      var s = el._ttc;
      if (s.corners.length) anime.utils.set(s.corners, { scale: 0, opacity: 0 });
      anime.utils.set([s.top, s.bottom], { scaleX: 0 });
      anime.utils.set(s.words, { y: '110%' });
      if (s.before.length) anime.utils.set(s.before, { opacity: 0, letterSpacing: '0.9em' });
      if (s.after.length) anime.utils.set(s.after, { opacity: 0, y: '0.6em' });
    }

    function play(el) {
      var s = el._ttc, d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      var tl = anime.createTimeline({
        delay: d.delay !== undefined ? +d.delay : DEFAULTS.delay,
        loop: loop,
        loopDelay: 500
      });
      if (s.corners.length) {
        tl.add(s.corners, { scale: [0, 1], opacity: [0, 1], duration: 500, delay: anime.stagger(70), ease: 'outBack(2)' });
      }
      tl.add(s.top, { scaleX: [0, 1], duration: 650, ease: 'inOut(3)' }, s.corners.length ? '-=350' : 0);
      if (s.before.length) {
        tl.add(s.before, { opacity: [0, 1], letterSpacing: ['0.9em', '0.3em'], duration: 900, ease: 'out(4)' }, '-=400');
      }
      tl.add(s.words, { y: ['110%', '0%'], duration: 750, delay: anime.stagger(80), ease: 'out(4)' }, s.before.length ? '-=700' : '-=250')
        .add(s.bottom, { scaleX: [0, 1], duration: 650, ease: 'inOut(3)' }, '-=650');
      if (s.after.length) {
        tl.add(s.after, { opacity: [0, 1], y: ['0.6em', '0em'], duration: 600, ease: 'out(3)' }, '-=300');
      }
      if (loop) {
        // Take it apart: words drop away, lines pull back to the centre.
        tl.add(s.words, { y: '-110%', duration: 450, delay: anime.stagger(40), ease: 'in(3)' }, '+=' + hold);
        var fade = s.before.concat(s.after);
        if (fade.length) tl.add(fade, { opacity: 0, duration: 350, ease: 'in(2)' }, '<<');
        tl.add([s.top, s.bottom], { scaleX: 0, duration: 450, ease: 'inOut(3)' }, '-=200');
        if (s.corners.length) tl.add(s.corners, { scale: 0, opacity: 0, duration: 300, ease: 'in(2)' }, '-=300');
      }
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-title-card]').forEach(function (el) {
        if (el._ttc) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-title-card data-color="#c084fc">
  <small>Episode 04</small>
  <h2>The Long Way Home</h2>
  <p>A film by Ava Moreno</p>
</div>