Word Morph
One word melts into the next. As the old word blurs away and the new one sharpens, a threshold filter keeps their edges crisp, so the letters seem to flow like liquid from one shape into another.
Made to flow
How to use it
- Paste the snippet just before
</head>. - Wrap the changing word in a
<span>and list every word, separated by commas, indata-word-morph.
Options
Add any of these attributes to the same element.
data-hold | How long each word stays. Default 1600 ms. |
data-duration | Time for each morph. Default 1100 ms. |
Works best on big, bold, solid-coloured text: the filter turns soft edges hard, so gradients and very thin fonts lose detail mid-morph.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Word Morph: one word melts into the next. As the old word blurs away and
// the new one sharpens, a threshold filter keeps their edges crisp, so the
// letters look like liquid flowing from one shape into another.
// Usage: <h1>Made to <span data-word-morph="flow, melt, morph">flow</span></h1>
// Optional: data-hold (ms per word, default 1600), data-duration (ms per
// morph, default 1100).
var DEFAULTS = { hold: 1600, duration: 1100 };
var FILTER = 'wmo-threshold';
var css =
'.wmo{display:inline-grid;vertical-align:baseline;text-align:left}' +
// Only while melting: at rest the words keep their smooth edges.
'.wmo-melting{filter:url(#' + FILTER + ')}' +
'.wmo-word{grid-area:1/1;white-space:nowrap}' +
'.wmo-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// The trick: blur both words, then snap every pixel to either fully opaque
// or fully clear. Where the two blurs overlap they fuse like droplets.
function addFilter() {
if (document.getElementById(FILTER)) return;
var div = document.createElement('div');
div.innerHTML =
'<svg width="0" height="0" style="position:absolute" aria-hidden="true">' +
'<filter id="' + FILTER + '" x="-20%" y="-40%" width="140%" height="180%">' +
'<feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 255 -140"/>' +
'</filter></svg>';
document.body.appendChild(div.firstChild);
}
function build(el) {
var words = (el.getAttribute('data-word-morph') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
el.classList.add('wmo');
el.textContent = '';
var sr = document.createElement('span');
sr.className = 'wmo-sr';
sr.textContent = words.join(', ');
el.appendChild(sr);
// Every word sits in the same grid cell, so the box is as wide as the
// longest one and the sentence never shifts.
var spans = words.map(function (w, i) {
var s = document.createElement('span');
s.className = 'wmo-word';
s.textContent = w;
s.setAttribute('aria-hidden', 'true');
if (i) s.style.opacity = 0;
el.appendChild(s);
return s;
});
return { el: el, spans: spans, index: 0, mix: 0 };
}
// mix runs 0 → 1 as the old word gives way to the new one. The blur grows
// very fast near the ends, so each word only becomes legible at the last
// moment, as in the classic morphing-text effect.
function paint(out, inn, mix) {
var a = Math.max(mix, 0.001), b = Math.max(1 - mix, 0.001);
inn.style.filter = 'blur(' + Math.min(8 / a - 8, 60).toFixed(2) + 'px)';
inn.style.opacity = Math.pow(mix, 0.4).toFixed(3);
out.style.filter = 'blur(' + Math.min(8 / b - 8, 60).toFixed(2) + 'px)';
out.style.opacity = Math.pow(1 - mix, 0.4).toFixed(3);
}
function start(s) {
var d = s.el.dataset;
(function next() {
var out = s.spans[s.index];
s.index = (s.index + 1) % s.spans.length;
var inn = s.spans[s.index];
s.mix = 0;
anime.animate(s, {
mix: 1,
duration: +d.duration || DEFAULTS.duration,
delay: +d.hold || DEFAULTS.hold,
ease: 'inOutSine',
onUpdate: function () {
s.el.classList.add('wmo-melting');
paint(out, inn, s.mix);
},
onComplete: function () {
s.el.classList.remove('wmo-melting');
out.style.filter = inn.style.filter = '';
next();
}
});
})();
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
start(e.target._wmo);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-word-morph]').forEach(function (el) {
if (el._wmo) return;
el._wmo = build(el);
// Reduced motion: the first word simply stays.
if (reduced || el._wmo.spans.length < 2) return;
addFilter();
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Made to
<span data-word-morph="flow, melt, morph">flow</span>
</h1>