Line-led Reveal
A line draws itself across, then the words rise up from behind it one after another, as if the line were the lip of a slot they climb out of.
Line it up
How to use it
- Paste the snippet just before
</head>. - Add
data-line-led-revealto any heading or text element.
Options
Add any of these attributes to the same element.
data-color | The line. Default #a855f7. |
data-thickness | The line’s thickness, in em (never thinner than 2px). Default 0.06. |
data-keep | false to draw the line away once the words are up. |
data-duration | How long each word takes to rise. Default 900 ms. |
data-stagger | Gap between words. Default 80 ms. |
data-loop | Draw, rise, hold, sink back and draw again. |
data-hold | How long the words stay up, when looping. Default 1800 ms. |
data-delay | Wait before starting. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Line-led Reveal: a line draws across under the text, then the words rise
// up from behind it, as if the line were the edge of a slot.
// Usage: <h1 data-line-led-reveal>Line it up</h1>
// Optional: data-color (the line, default "#a855f7"), data-thickness (in em,
// default 0.06), data-keep ("false" to draw the line away once the text is
// up), data-duration (ms for the words to rise, default 900), data-stagger
// (ms between words, default 80), data-loop (words sink, line undraws, and
// again), data-hold (ms to stay, when looping), data-delay (ms).
var DEFAULTS = { color: '#a855f7', thickness: 0.06, duration: 900, stagger: 80, hold: 1800, delay: 0 };
var css =
'.llr-box{display:inline-block;position:relative;padding-bottom:.1em;max-width:100%}' +
'.llr-clip{display:block;padding-bottom:.16em;clip-path:inset(-1em -.5em 0 -.5em)}' +
'.llr-w{display:inline-block;white-space:nowrap;will-change:transform}' +
'.llr-line{position:absolute;left:0;right:0;bottom:0;border-radius:99px;transform-origin:0 50%}';
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();
el.setAttribute('aria-label', text);
el.textContent = '';
var box = document.createElement('span');
box.className = 'llr-box';
box.setAttribute('aria-hidden', 'true');
var clip = document.createElement('span');
clip.className = 'llr-clip';
var words = text.split(' ').map(function (word, i) {
if (i) clip.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'llr-w';
w.textContent = word;
clip.appendChild(w);
return w;
});
var line = document.createElement('span');
line.className = 'llr-line';
line.style.background = d.color || DEFAULTS.color;
line.style.height = 'max(2px, ' + (+d.thickness || DEFAULTS.thickness) + 'em)';
box.appendChild(clip);
box.appendChild(line);
el.appendChild(box);
return { clip: clip, words: words, line: line };
}
// How far each word must drop to sit just below the line.
function depth(s, w) {
return s.clip.offsetHeight - w.offsetTop + 4;
}
function play(el) {
var s = el._llr, d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var keep = d.keep !== 'false';
var duration = +d.duration || DEFAULTS.duration;
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var tl = anime.createTimeline({
onComplete: function () { if (loop) setTimeout(function () { play(el); }, 400); }
});
s.line.style.transformOrigin = '0 50%';
tl.add(s.line, { scaleX: [0, 1], duration: 650, ease: 'inOutQuart' }, 0);
var t = 530, n = s.words.length;
s.words.forEach(function (w, i) {
tl.add(w, { y: [depth(s, w), 0], duration: duration, ease: 'outQuart' }, t + i * stagger);
});
var up = t + (n - 1) * stagger + duration;
var unline = { scaleX: [1, 0], duration: 500, ease: 'inOutQuart', onBegin: function () { s.line.style.transformOrigin = '100% 50%'; } };
if (!keep) tl.add(s.line, unline, up - 200);
if (loop) {
var out = up + (+d.hold || DEFAULTS.hold);
s.words.forEach(function (w, i) {
tl.add(w, { y: [0, depth(s, w)], duration: duration * 0.55, ease: 'inQuart' }, out + i * stagger * 0.5);
});
if (keep) tl.add(s.line, unline, out + (n - 1) * stagger * 0.5 + duration * 0.4);
}
}
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.2 });
document.querySelectorAll('[data-line-led-reveal]').forEach(function (el) {
if (el._llr) return;
var s = el._llr = setup(el);
if (reduced()) {
if (el.dataset.keep === 'false') s.line.style.transform = 'scaleX(0)';
return;
}
s.line.style.transform = 'scaleX(0)';
s.words.forEach(function (w) { w.style.transform = 'translateY(' + depth(s, w) + 'px)'; });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-line-led-reveal>Line it up</h1>
<!-- or, with a thin dark line that leaves afterwards: -->
<h1 data-line-led-reveal data-color="#111" data-thickness="0.03" data-keep="false">Line it up</h1>