Variable-weight Wave
A swell of boldness rolls through the letters: each one thickens as the crest passes, then thins out again behind it. It bends the weight axis of a variable font, so the strokes really grow rather than just scaling.
Weight for it
How to use it
- Paste the snippet just before
</head>. - Use a variable font with a weight axis, such as Inter, Roboto Flex or Recursive.
- Add
data-variable-weight-waveto a heading.
Options
Add any of these attributes to the same element.
data-min | The thinnest weight. Default 200. |
data-max | The boldest weight, at the crest. Default 900. |
data-speed | Time for one swell. Default 1600 ms. |
data-stagger | Gap between letters: smaller is a longer wave. Default 90 ms. |
data-color | Colour letters turn as the crest passes. |
With a regular (non-variable) font the weight can only jump between the weights you have loaded. Bolder letters are wider, so centred text gently breathes as the wave passes.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Variable-weight Wave: a swell of boldness rolls through the letters, each
// one thickening as the crest passes and thinning out again behind it.
// Needs a variable font with a weight axis (Inter, Roboto Flex, Recursive…).
// Usage: <h1 data-variable-weight-wave>Heavy lifting</h1>
// Optional: data-min (thinnest weight, default 200), data-max (boldest
// weight, default 900), data-speed (ms for one swell, default 1600),
// data-stagger (ms between letters, default 90), data-color (letters take
// this colour at the crest).
var DEFAULTS = { min: 200, max: 900, speed: 1600, stagger: 90 };
var css =
'.vww-word{display:inline-block;white-space:nowrap}' +
".vww-char{display:inline-block;font-variation-settings:'wght' var(--vww-w)}";
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function split(el, min) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var chars = [];
text.split(' ').forEach(function (w, i) {
if (i) el.appendChild(document.createTextNode(' '));
var word = document.createElement('span');
word.className = 'vww-word';
word.setAttribute('aria-hidden', 'true');
Array.from(w).forEach(function (c) {
var s = document.createElement('span');
s.className = 'vww-char';
s.textContent = c;
s.style.setProperty('--vww-w', min);
word.appendChild(s);
chars.push(s);
});
el.appendChild(word);
});
return chars;
}
function start(el, chars, min, max) {
var d = el.dataset;
var props = {
'--vww-w': [min, max, min],
duration: +d.speed || DEFAULTS.speed,
ease: 'inOutSine',
loop: true,
// Each letter swells a little later, so the bold crest rolls along the line.
delay: anime.stagger(+d.stagger || DEFAULTS.stagger)
};
if (d.color) {
var base = getComputedStyle(el).color;
props.color = [base, d.color, base];
}
return anime.animate(chars, props);
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-variable-weight-wave]').forEach(function (el) {
if (el._vww) return;
el._vww = true;
// Reduced motion: the text keeps its own weight and stays still.
if (reduced) return;
var d = el.dataset;
var min = +d.min || DEFAULTS.min;
var max = +d.max || DEFAULTS.max;
var chars = split(el, min);
var anim = null;
// Only roll while visible, to save battery.
new IntersectionObserver(function (entries) {
if (!entries[0].isIntersecting) { if (anim) anim.pause(); return; }
if (anim) anim.play();
else anim = start(el, chars, min, max);
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-variable-weight-wave>Heavy lifting</h1>
<!-- a subtler wave that turns purple at the crest: -->
<h1 data-variable-weight-wave data-min="400" data-max="800" data-color="#a855f7">Heavy lifting</h1>