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
- Paste the snippet just before
</head>. - Add
data-end-credits-rollto a box with a height, holding your credits. - 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-speed | Rolling speed in pixels per second. Default 40. |
data-loop | Start again from the bottom when the last line has gone. |
data-start | How 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.
<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><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>