Marching Ants
A dashed outline crawls steadily round a shape, like a selection in a photo editor. Put it on any box (a drop zone, a card, a crop area) and the dashes follow its rounded corners, or on an SVG to march the outlines of its shapes.
Drop files to upload
or click to browse
How to use it
- Paste the snippet just before
</head>. - Add
data-marching-antsto an element for a marching border round its edge, rounded corners included. - Or add it to an
<svg>: every shape with astrokemarches. Mark shapes withdata-antsto march only those.
Options
Add any of these attributes to the same element.
data-dash | Dash length. Default 8. |
data-gap | Gap between dashes. Default 6. |
data-speed | Pixels per second. Default 24. |
data-under | A colour that shows in the gaps, e.g. #fff under black dashes for the classic selection look. |
data-color | Dash colour, on elements. Default the text colour. |
data-width | Line width in px, on elements. Default 2. |
data-reverse | March the other way. |
The dashes are stretched a touch so a whole number fits round the shape, with no seam. They only march while on screen; with reduced motion they stay still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Marching Ants: a dashed outline crawls round a shape, like a selection in
// a photo editor. Put it on any element (a drop zone, a card, a box over a
// photo) for a marching border that follows its rounded corners, or on an
// SVG to march the outlines of its shapes.
// Usage: <div data-marching-ants>Drop files here</div>
// or: <svg data-marching-ants viewBox="…"><ellipse … fill="none" stroke="#111"/></svg>
// In an SVG every shape with a stroke marches; mark some with data-ants to
// march only those. Dashes are stretched a touch so they meet evenly.
// Optional: data-dash (dash length, default 8), data-gap (default 6),
// data-speed (px per second, default 24), data-reverse (march the other
// way), data-under (a colour shown in the gaps, e.g. #fff for the classic
// black-and-white selection). On elements: data-color (default the text
// colour), data-width (border width, default 2).
var DEFAULTS = { dash: 8, gap: 6, speed: 24, width: 2 };
var NS = 'http://www.w3.org/2000/svg';
var SHAPES = 'path, rect, circle, ellipse, line, polyline, polygon';
var css =
'.ma-ring{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;fill:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var d = el.dataset;
var dash = +d.dash || DEFAULTS.dash;
var gap = +d.gap || DEFAULTS.gap;
var period = dash + gap;
var lines = [];
if (el instanceof SVGSVGElement) {
var marked = el.querySelectorAll('[data-ants]');
lines = Array.from(marked.length ? marked : el.querySelectorAll(SHAPES)).filter(function (s) {
return !s.closest('defs, clipPath, mask') && (marked.length || getComputedStyle(s).stroke !== 'none');
});
if (d.under) {
// A solid copy underneath shows through the gaps.
lines.forEach(function (s) {
var u = s.cloneNode(false);
u.removeAttribute('data-ants');
u.removeAttribute('id');
u.setAttribute('aria-hidden', 'true');
u.style.stroke = d.under;
u.style.strokeDasharray = 'none';
s.parentNode.insertBefore(u, s);
});
}
} else {
// An outline drawn in an SVG laid over the element, matching its corners.
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('class', 'ma-ring');
svg.setAttribute('aria-hidden', 'true');
var width = +d.width || DEFAULTS.width;
var make = function (color) {
var r = document.createElementNS(NS, 'rect');
r.style.stroke = color;
r.style.strokeWidth = width;
svg.appendChild(r);
return r;
};
var under = d.under ? make(d.under) : null;
var rect = make(d.color || 'currentColor');
el.appendChild(svg);
lines = [rect];
var fit = function () {
var w = el.offsetWidth, h = el.offsetHeight;
var cs = getComputedStyle(el);
var radius = parseFloat(cs.borderTopLeftRadius) || 0;
if (/%/.test(cs.borderTopLeftRadius)) radius = (Math.min(w, h) * radius) / 100;
[rect, under].forEach(function (r) {
if (!r) return;
// Centre the line on the element's edge, inside its box.
r.setAttribute('x', width / 2);
r.setAttribute('y', width / 2);
r.setAttribute('width', Math.max(0, w - width));
r.setAttribute('height', Math.max(0, h - width));
r.setAttribute('rx', Math.max(0, Math.min(radius - width / 2, (w - width) / 2)));
r.setAttribute('ry', Math.max(0, Math.min(radius - width / 2, (h - width) / 2)));
});
measure();
paint();
};
}
if (!lines.length) return;
// Stretch each line's dashes so a whole number fits round it: no seam.
var scale = [];
function measure() {
lines.forEach(function (s, i) {
var len = s.getTotalLength ? s.getTotalLength() : 0;
var n = Math.max(1, Math.round(len / period));
scale[i] = len ? len / (n * period) : 1;
s.style.strokeDasharray = dash * scale[i] + ' ' + gap * scale[i];
});
}
var march = { offset: 0 };
function paint() {
lines.forEach(function (s, i) { s.style.strokeDashoffset = -march.offset * scale[i]; });
}
if (fit) {
fit();
if (window.ResizeObserver) new ResizeObserver(fit).observe(el);
} else {
measure();
paint();
}
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var reverse = d.reverse !== undefined && d.reverse !== 'false';
var anim = anime.animate(march, {
offset: reverse ? [period, 0] : [0, period],
duration: (period / (+d.speed || DEFAULTS.speed)) * 1000,
ease: 'linear',
loop: true,
autoplay: false,
onUpdate: paint
});
// March only while on screen.
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) anim.play(); else anim.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-marching-ants]').forEach(function (el) {
if (el._ma) return;
el._ma = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="drop-zone" data-marching-ants data-color="#7c3aed">
Drop files to upload
</div>
<!-- a selection over a photo: -->
<svg data-marching-ants data-under="#fff" viewBox="0 0 400 250">
<ellipse cx="300" cy="60" rx="40" ry="40" fill="none" stroke="#111" stroke-width="1.5"/>
</svg>