← Broadcast

End Credits Roll

Credits roll up the screen at a steady pace, fading in at the bottom edge and out at the top. Roles and names line up either side of a centre gap, like the end of a film. Point at it, or press and hold on a phone, to pause.

A film by

Ava Moreno

Director
Ava Moreno
Producer
Sam Lee
Writer
Priya Nair
Camera
Jonah Weiss
Editor
Lena Fox
Music
Tomás Ruiz
Sound
Kemi Adeyemi
Colour
Rui Tanaka

Starring

Noah Bright

Ellie Park

Marcus Hale

Thanks for watching

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-end-credits-roll to a box with a height, holding your credits.
  3. Headings become small spaced-out titles; a <dl> of <dt> roles and <dd> names lines up in two columns.

Options

Add any of these attributes to the same element.

data-speedRolling speed in pixels per second. Default 40.
data-loopStart again from the bottom when the last line has gone.
data-startHow much of the box is already filled when it starts, from 0 to 1. Default 0.
data-pause="false"Keep rolling when pointed at or touched.

With reduced motion the credits sit still and the box scrolls instead.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // End Credits Roll: credits roll up the screen at a steady pace, fading
    // in at the bottom edge and out at the top. Roles and names in a <dl> line
    // up either side of a centre gap, like film credits. Point at it (or press
    // and hold on a phone) to pause.
    // Usage: <div data-end-credits-roll style="height: 320px">
    //          <h3>Directed by</h3><p>Ava Moreno</p>
    //          <dl><dt>Producer</dt><dd>Sam Lee</dd>…</dl>
    //        </div>
    // Give it a height; the content scrolls inside it.
    // Optional: data-speed (px per second, default 40), data-loop (start
    // again from the bottom when it's done), data-start (how much of the box
    // is already filled when it starts, 0 to 1, default 0), data-pause="false"
    // (don't pause on hover or touch).
    var DEFAULTS = { speed: 40 };

    var css =
      '.ecr{overflow:hidden;-webkit-mask-image:linear-gradient(transparent,#000 14%,#000 86%,transparent);mask-image:linear-gradient(transparent,#000 14%,#000 86%,transparent)}' +
      '.ecr-roll{text-align:center;will-change:transform}' +
      '.ecr-roll>:first-child{margin-top:0}' +
      '.ecr-roll h1,.ecr-roll h2,.ecr-roll h3,.ecr-roll h4{margin:2.2em 0 0.5em;font-size:0.8em;font-weight:600;text-transform:uppercase;letter-spacing:0.22em;opacity:.65}' +
      '.ecr-roll p{margin:0 0 0.3em;font-weight:700}' +
      '.ecr-roll dl{display:grid;grid-template-columns:1fr 1fr;gap:0.45em 1.4em;margin:2em 0}' +
      '.ecr-roll dt{text-align:right;font-size:0.78em;text-transform:uppercase;letter-spacing:0.14em;opacity:.65;align-self:center}' +
      '.ecr-roll dd{margin:0;text-align:left;font-weight:600}' +
      '.ecr-still{overflow:auto}';
    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;
      el.classList.add('ecr');
      var roll = document.createElement('div');
      roll.className = 'ecr-roll';
      while (el.firstChild) roll.appendChild(el.firstChild);
      el.appendChild(roll);

      // Without motion, it's simply a scrollable list.
      if (reduced()) { el.classList.add('ecr-still'); el.tabIndex = 0; return; }

      var speed = +d.speed || DEFAULTS.speed;
      var loop = d.loop !== undefined && d.loop !== 'false';
      // offset: how far the roll has travelled up from just below the bottom edge.
      var offset = Math.max(0, Math.min(1, +d.start || 0)) * el.clientHeight;
      var last = 0, held = false, done = false;

      function place() {
        roll.style.transform = 'translateY(' + (el.clientHeight - offset).toFixed(2) + 'px)';
      }
      place();

      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          if (held || done) return;
          offset += (speed * dt) / 1000;
          var end = el.clientHeight + roll.offsetHeight;
          if (offset >= end) {
            if (loop) offset -= end;
            else { offset = end; done = true; }
          }
          place();
        }
      });

      if (d.pause !== 'false') {
        el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') held = true; });
        el.addEventListener('pointerleave', function () { held = false; });
        el.addEventListener('pointerdown', function () { held = true; });
        el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') held = false; });
        el.addEventListener('pointercancel', function () { held = false; });
      }
      if (window.ResizeObserver) new ResizeObserver(place).observe(el);

      // Roll only while on screen.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }, { threshold: 0.1 }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-end-credits-roll]').forEach(function (el) {
        if (el._ecr) return;
        el._ecr = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-end-credits-roll style="height: 320px">
  <h3>A film by</h3>
  <p>Ava Moreno</p>
  <dl>
    <dt>Producer</dt><dd>Sam Lee</dd>
    <dt>Writer</dt><dd>Priya Nair</dd>
    <dt>Editor</dt><dd>Lena Fox</dd>
  </dl>
  <h3>Thanks for watching</h3>
</div>