Light Trail Logo
Comets of light race round the outlines of a logo, speeding up and slowing down, then fade away as the outline lights up and is left glowing like neon. Any wordmark in the SVG glows in at the end. Best on a dark background.
How to use it
- Paste the snippet just before
</head>. - Add
data-light-trail-logoto your logo’s inline<svg>, drawn as outlines: shapes with astroke. - Trails race round every shape. Text in the SVG has no outline to follow, so it glows in at the end instead.
Options
Add any of these attributes to the same element.
data-color | Colour of the trails and glow. Default each shape’s own stroke colour. |
data-trails | Trails chasing round each shape. Default 2. |
data-laps | How far they go round. Default 1.25 laps. |
data-duration | Time for the race. Default 2400 ms. |
data-loop | Race, glow, dim and repeat. |
data-hold | How long it glows, when looping. Default 2400 ms. |
data-delay | Wait before it starts. |
Shapes with a fill fill in as the outline lights up. It starts when scrolled into view. With reduced motion the logo simply shows, glowing.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Light Trail Logo: comets of light race round the outlines of an SVG logo,
// speeding up and slowing down, then fade as the outline lights up and is
// left glowing. Any text in the SVG glows in at the end. Loop it and the
// logo dims before the trails race round again. Looks best on a dark
// background.
// Usage: <svg data-light-trail-logo viewBox="0 0 120 120" fill="none" stroke="#c084fc" stroke-width="4">
// <polygon points="60,8 105,34 105,86 60,112 15,86 15,34"/>
// </svg>
// Optional: data-color (trail colour, default each shape's stroke),
// data-trails (trails per shape, default 2), data-laps (times round,
// default 1.25), data-duration (ms for the race, default 2400),
// data-delay (ms before it starts), data-loop (race, glow, dim and
// repeat), data-hold (ms it glows when looping, default 2400).
var DEFAULTS = { trails: 2, laps: 1.25, duration: 2400, hold: 2400 };
var SHAPES = 'path, line, polyline, polygon, circle, ellipse, rect';
var NS = 'http://www.w3.org/2000/svg';
// A trail is three layers: a long faint tail, a middle and a white-hot head.
var LAYERS = [{ len: 0.34, w: 0.8, op: 0.35 }, { len: 0.16, w: 1.1, op: 0.75 }, { len: 0.022, w: 1.15, op: 0.95, core: true }];
var count = 0;
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function make(tag, attrs) {
var n = document.createElementNS(NS, tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function setup(svg) {
var d = svg.dataset;
var shapes = Array.prototype.filter.call(svg.querySelectorAll(SHAPES), function (s) {
return !s.closest('defs, clipPath, mask, [data-ltl-skip]') && typeof s.getTotalLength === 'function';
});
var texts = Array.prototype.slice.call(svg.querySelectorAll('text'));
if (!shapes.length) return null;
var vb = svg.viewBox.baseVal;
var size = vb && vb.width ? Math.max(vb.width, vb.height) : 100;
var id = 'ltl-glow-' + ++count;
var defs = make('defs', {});
defs.innerHTML = '<filter id="' + id + '" x="-30%" y="-30%" width="160%" height="160%">' +
'<feGaussianBlur stdDeviation="' + (size * 0.012).toFixed(2) + '" result="b"/>' +
'<feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>' +
// A softer glow for text, so it stays crisp.
'<filter id="' + id + 't" x="-10%" y="-30%" width="120%" height="160%">' +
'<feGaussianBlur stdDeviation="' + (size * 0.005).toFixed(2) + '" result="b"/>' +
'<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
svg.insertBefore(defs, svg.firstChild);
var n = Math.max(1, +d.trails || DEFAULTS.trails);
var trails = [], glows = [];
var trailGroup = make('g', { filter: 'url(#' + id + ')', 'aria-hidden': 'true', fill: 'none', 'stroke-linecap': 'round' });
var glowGroup = make('g', { filter: 'url(#' + id + ')', 'aria-hidden': 'true', fill: 'none' });
shapes.forEach(function (s) {
var cs = getComputedStyle(s);
var color = d.color || (cs.stroke !== 'none' ? cs.stroke : '#fff');
var width = parseFloat(cs.strokeWidth) || 2;
var L = s.getTotalLength();
var glow = s.cloneNode(false);
glow.removeAttribute('id');
glow.setAttribute('stroke', color);
glow.setAttribute('stroke-width', width);
glow.style.fill = 'none';
glow.style.opacity = 0;
glowGroup.appendChild(glow);
glows.push(glow);
for (var k = 0; k < n; k++) {
LAYERS.forEach(function (layer) {
var seg = Math.min(L * layer.len, L / n * 0.9);
var t = s.cloneNode(false);
t.removeAttribute('id');
t.setAttribute('stroke', layer.core ? '#fff' : color);
t.setAttribute('stroke-width', width * layer.w);
t.style.fill = 'none';
t.style.strokeDasharray = seg + ' ' + (L - seg);
t.style.opacity = 0;
trailGroup.appendChild(t);
trails.push({ el: t, L: L, seg: seg, start: k * L / n, op: layer.op });
});
}
// The outline itself starts dim; its fill (if any) starts hidden.
s.style.strokeOpacity = 0.16;
s.style.fillOpacity = 0;
});
svg.appendChild(glowGroup);
svg.appendChild(trailGroup);
texts.forEach(function (t) {
t.setAttribute('filter', 'url(#' + id + 't)');
t.style.opacity = 0;
});
return { svg: svg, shapes: shapes, texts: texts, trails: trails, glows: glows, state: { p: 0 } };
}
function render(o) {
var p = o.state.p;
o.trails.forEach(function (t) {
// The head of each trail is p laps round, plus its own starting point.
t.el.style.strokeDashoffset = -(p * t.L + t.start - t.seg);
});
}
function lit(o) {
o.shapes.forEach(function (s) { s.style.strokeOpacity = 1; s.style.fillOpacity = ''; });
o.glows.forEach(function (g) { g.style.opacity = 0.7; });
o.texts.forEach(function (t) { t.style.opacity = 1; });
}
function run(o) {
var d = o.svg.dataset;
var duration = +d.duration || DEFAULTS.duration;
var laps = +d.laps || DEFAULTS.laps;
var hold = +d.hold || DEFAULTS.hold;
var loop = d.loop !== undefined && d.loop !== 'false';
var trailEls = o.trails.map(function (t) { return t.el; });
var draw = function () { render(o); };
var race = function () {
o.state.p = 0;
render(o);
var tl = anime.createTimeline({ onComplete: function () { if (loop) setTimeout(dim, hold); } });
tl.add(trailEls, { opacity: function (el, i) { return o.trails[i].op; }, duration: 300, ease: 'out(2)' }, 0)
.add(o.state, { p: laps, duration: duration, ease: 'inOut(2)', onUpdate: draw }, 0)
// As the trails come home, the outline lights up and they fade.
.add(o.shapes, { strokeOpacity: 1, duration: 700, ease: 'out(2)' }, duration * 0.72)
.add(o.glows, { opacity: [{ to: 1, duration: 450, ease: 'out(2)' }, { to: 0.7, duration: 900, ease: 'inOut(2)' }] }, duration * 0.78)
.add(trailEls, { opacity: 0, duration: 600, ease: 'out(2)' }, duration * 0.85)
.add(o.shapes, { fillOpacity: 1, duration: 800, ease: 'out(2)' }, duration * 0.85);
if (o.texts.length) tl.add(o.texts, { opacity: [0, 1], duration: 900, ease: 'out(2)', delay: anime.stagger(120) }, duration * 0.82);
};
var dim = function () {
var tl = anime.createTimeline({ onComplete: function () { setTimeout(race, 300); } });
tl.add(o.glows, { opacity: 0, duration: 700, ease: 'inOut(2)' }, 0)
.add(o.shapes, { strokeOpacity: 0.16, fillOpacity: 0, duration: 700, ease: 'inOut(2)' }, 0);
if (o.texts.length) tl.add(o.texts, { opacity: 0, duration: 600, ease: 'inOut(2)' }, 0);
};
setTimeout(race, +d.delay || 200);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
run(e.target._ltl);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-light-trail-logo]').forEach(function (svg) {
if (svg._ltl) return;
var o = setup(svg);
svg._ltl = o || true;
if (!o) return;
if (reduced()) lit(o);
else io.observe(svg);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><svg data-light-trail-logo viewBox="0 0 120 120" fill="none"
stroke="#c084fc" stroke-width="4" stroke-linejoin="round" role="img" aria-label="Your brand">
<polygon points="60,10 103,35 103,85 60,110 17,85 17,35"/>
<path d="M39 84 L60 36 L81 84"/>
<path d="M47.5 67 H72.5"/>
</svg>