Kinetic Poster Type
Words slam in one at a time, each line sized to fill the full width, until they lock into a tight block like a type poster. Short lines come out huge, long ones smaller, and every hit jolts the whole poster.
Make it big loud and bold
How to use it
- Paste the snippet just before
</head>. - Add
data-kinetic-poster-typeto a short phrase in a heavy font. - Give it a height to make the poster fit inside it; without one it sizes to the width.
Options
Add any of these attributes to the same element.
data-colors | Line colours, comma-separated, cycled line by line. Default currentColor,#a855f7. |
data-width | The widest the block gets, in px. Default 560. |
data-max | The biggest a line may be, as a share of the width. Lower it for more, smaller lines. Default 0.42. |
data-interval | Gap between words. Default 260 ms. |
data-loop | Clear the poster and slam it in again. |
data-hold | How long the poster stays, when looping. Default 1800 ms. |
data-delay | Wait before starting. Default 0 ms. |
It looks best in capitals: add text-transform: uppercase to the element.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Kinetic Poster Type: words slam in one at a time, each line sized to
// fill the full width, until they build a tight block like a type poster.
// Short lines come out huge, long ones smaller.
// Usage: <h1 data-kinetic-poster-type>MAKE IT BIG AND BOLD</h1>
// Give the element a height and the poster scales to fit inside it.
// Optional: data-colors (comma-separated, cycled line by line, default
// "currentColor,#a855f7"), data-width (widest the block gets, px, default
// 560), data-max (biggest a line may be, as a share of the width, default
// 0.42), data-interval (ms between words, default 260), data-loop (clear
// and slam again), data-hold (ms to stay, when looping, default 1800),
// data-delay (ms).
var DEFAULTS = { colors: 'currentColor,#a855f7', width: 560, max: 0.42, interval: 260, hold: 1800, delay: 0 };
var LEADING = 0.9;
var css =
'.kpt{display:flex;align-items:center;justify-content:center}' +
'.kpt-block{display:block;flex:none;text-align:center}' +
'.kpt-line{display:block;white-space:nowrap;line-height:' + LEADING + ';letter-spacing:-.03em}' +
'.kpt-w{display:inline-block;will-change:transform,opacity}' +
'.kpt-w+.kpt-w{margin-left:.2em}';
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 text = el.textContent.replace(/\s+/g, ' ').trim();
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
var max = +d.max || DEFAULTS.max;
el.setAttribute('aria-label', text);
el.textContent = '';
el.classList.add('kpt');
// An element with its own height (from CSS) is a frame to fit inside.
var fixed = el.clientHeight > 0;
var block = document.createElement('span');
block.className = 'kpt-block';
block.setAttribute('aria-hidden', 'true');
el.appendChild(block);
// Measure each candidate line at 100px to find the font size that fills
// the width, as a share of the width. Add words to a line until it is
// no bigger than data-max.
var probe = document.createElement('span');
probe.className = 'kpt-line';
probe.style.cssText = 'display:inline-block;font-size:100px;position:absolute;visibility:hidden';
el.appendChild(probe);
function ratio(ws) {
probe.innerHTML = ws.map(function (w) { return '<span class="kpt-w">' + w.replace(/</g, '<') + '</span>'; }).join('');
return 100 / Math.max(1, probe.offsetWidth);
}
var groups = [], cur = [];
text.split(' ').forEach(function (w, i, all) {
cur.push(w);
if (ratio(cur) <= max || i === all.length - 1) {
groups.push({ words: cur, r: Math.min(max, ratio(cur)) });
cur = [];
}
});
el.removeChild(probe);
var words = [];
groups.forEach(function (g, i) {
var line = document.createElement('span');
line.className = 'kpt-line';
line.style.color = colors[i % colors.length];
g.words.forEach(function (w) {
var s = document.createElement('span');
s.className = 'kpt-w';
s.textContent = w;
line.appendChild(s);
words.push(s);
});
block.appendChild(line);
g.line = line;
});
var s = { block: block, groups: groups, words: words, fixed: fixed, el: el };
layout(s);
return s;
}
// Size the block to the element (and its height, if it has one), then
// every line to fill the block's width.
function layout(s) {
var d = s.el.dataset;
var w = Math.min(s.el.clientWidth, +d.width || DEFAULTS.width);
var sum = s.groups.reduce(function (a, g) { return a + g.r; }, 0);
if (s.fixed && sum) w = Math.min(w, s.el.clientHeight / (LEADING * sum));
s.block.style.width = w + 'px';
s.groups.forEach(function (g) { g.line.style.fontSize = (g.r * w * 0.995).toFixed(2) + 'px'; });
}
function play(el) {
var s = el._kpt, d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var interval = +d.interval || DEFAULTS.interval;
var tl = anime.createTimeline({
onComplete: function () { if (loop) setTimeout(function () { play(el); }, 300); }
});
anime.utils.set(s.words, { opacity: 0 });
s.words.forEach(function (w, i) {
var t = i * interval;
// Each word drops in from oversized, hits hard and settles.
tl.add(w, {
opacity: [0, 1],
scale: [2.6, 1],
rotate: [anime.utils.random(-12, 12), 0],
duration: 230,
ease: 'in(2)'
}, t);
// The impact jolts the whole poster.
tl.add(s.block, { y: [0, 3, -1.5, 0], x: [0, i % 2 ? 2 : -2, 0], duration: 180, ease: 'out(2)' }, t + 220);
});
if (loop) {
var out = (s.words.length - 1) * interval + 400 + (+d.hold || DEFAULTS.hold);
tl.add(s.words, {
opacity: 0,
scale: 0.7,
duration: 260,
delay: anime.stagger(35, { from: 'last' }),
ease: 'inQuad'
}, out);
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
var el = e.target;
setTimeout(function () { play(el); }, +el.dataset.delay || DEFAULTS.delay);
});
}, { threshold: 0.3 });
var ro = new ResizeObserver(function (entries) {
entries.forEach(function (e) { if (e.target._kpt) layout(e.target._kpt); });
});
var run = function () {
document.querySelectorAll('[data-kinetic-poster-type]').forEach(function (el) {
if (el._kpt) return;
var s = el._kpt = setup(el);
ro.observe(el);
if (reduced()) return;
s.words.forEach(function (w) { w.style.opacity = 0; });
io.observe(el);
});
};
// Lines are measured, so wait for the web font.
if (document.fonts && document.fonts.ready) document.fonts.ready.then(run);
else run();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-kinetic-poster-type style="text-transform: uppercase">Make it big loud and bold</h1>
<!-- or, in three colours, inside a fixed frame: -->
<h1 data-kinetic-poster-type data-colors="#111,#e11d48,#a855f7" style="height: 320px">Make it big loud and bold</h1>