Tracking In
Text fades in from a soft blur while its letter spacing tightens from wide to normal, drawing the letters together. Short headings stay on one line and stay centred the whole way.
Breathe in
How to use it
- Paste the snippet just before
</head>. - Add
data-tracking-into any heading or text element.
Options
Add any of these attributes to the same element.
data-loop | Tighten in, hold, open back out as it fades, and in again. |
data-hold | How long the text stays before fading, when looping. Default 1800 ms. |
data-spacing | How much wider the spacing starts, in em. Default 0.5. |
data-duration | How long the spacing takes to tighten. Default 1400 ms. |
data-delay | Wait before starting. Default 0 ms. |
Looks especially good on short uppercase headings.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Tracking In: text fades in from a soft blur while its letter spacing
// tightens from wide to normal, drawing the letters together.
// Usage: <h1 data-tracking-in>Breathe in</h1>
// Optional: data-loop (spacing opens back out as the text fades, then
// tightens again), data-hold (ms to stay before fading), data-spacing (how
// wide it starts, in em; default 0.5), data-duration (ms), data-delay (ms
// before starting).
var DEFAULTS = { spacing: 0.5, duration: 1400, delay: 0, hold: 1800 };
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// The element's own letter spacing, in em, so it scales with the font.
function baseSpacing(el) {
var cs = getComputedStyle(el);
var px = parseFloat(cs.letterSpacing) || 0;
return px / parseFloat(cs.fontSize);
}
// True when the text sits on one line at rest. Then it's kept on one line
// while the spacing is wide, instead of wrapping and jumping about.
function fitsOneLine(el) {
var h = el.offsetHeight;
el.style.whiteSpace = 'nowrap';
var one = el.offsetHeight === h;
el.style.whiteSpace = '';
return one;
}
function play(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var duration = +d.duration || DEFAULTS.duration;
var base = baseSpacing(el);
var wide = base + (+d.spacing || DEFAULTS.spacing);
var oneLine = fitsOneLine(el);
// Letter spacing also adds space after the last letter, which pushes
// centred text left. An equal indent on the left keeps it centred.
var centred = oneLine && getComputedStyle(el).textAlign === 'center';
if (oneLine) el.style.whiteSpace = 'nowrap';
var into = {
letterSpacing: [wide + 'em', base + 'em'],
opacity: [0, 1],
filter: ['blur(8px)', 'blur(0px)'],
duration: duration,
ease: 'outExpo'
};
if (centred) into.textIndent = [wide - base + 'em', '0em'];
var tl = anime.createTimeline({
loop: loop,
loopDelay: 300,
delay: +d.delay || DEFAULTS.delay,
onComplete: function () {
// Hand the spacing back to the stylesheet.
['letterSpacing', 'textIndent', 'filter', 'whiteSpace'].forEach(function (p) { el.style[p] = ''; });
}
});
tl.add(el, into);
if (loop) {
var out = {
letterSpacing: wide + 'em',
opacity: 0,
filter: 'blur(8px)',
duration: duration * 0.6,
ease: 'inQuad'
};
if (centred) out.textIndent = wide - base + 'em';
tl.add(el, out, '+=' + (+d.hold || DEFAULTS.hold));
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-tracking-in]').forEach(function (el) {
if (el._trk) return;
el._trk = true;
if (reduced()) return;
el.style.opacity = 0;
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-tracking-in>Breathe in</h1>
<!-- or, looping: -->
<h1 data-tracking-in data-loop>Breathe in</h1>