Tug-of-war Bar
Two colours push against each other along a bar, for stream polls, predictions and versus battles. Stripes on each side flow towards the front where they meet, the front springs to each new share with a spray of sparks, and arrows mark the halfway line. Each side shows its name and share.
Who takes the final?
How to use it
- Paste the snippet just before
</head>. - Add
data-tug-of-war-barto an empty element, with the side names indata-leftanddata-right. It fills the width of its container; size it withfont-size. - Call
animacious.tugOfWarBar(el, 62)to move the front to 62% for the left side.
Options
Add any of these attributes to the same element.
data-left, data-right | Names for each side. |
data-value | The left side’s starting share, from 0 to 100. Default 50. |
data-left-color, data-right-color | Side colours. Default #a855f7 and #f59e0b. |
data-autoplay | The sides push back and forth on their own, for demos. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Tug-of-war Bar: two colours push against each other along a bar, for
// stream polls, predictions and versus battles. Stripes flow towards the
// front where they meet, the front springs to each new share with a flash
// of sparks, and a marker shows the halfway line. Each side shows its name
// and share.
// Usage: <div data-tug-of-war-bar data-left="Cats" data-right="Dogs" data-value="55"></div>
// Size it with font-size; it fills the width of its container.
// Optional: data-left, data-right (side names), data-value (left side's
// share, 0 to 100, default 50), data-left-color (default #a855f7),
// data-right-color (default #f59e0b), data-autoplay (the sides push back
// and forth on their own, for demos).
// Script use: animacious.tugOfWarBar(el, 62) moves the front to 62% left.
var DEFAULTS = { value: 50, leftColor: '#a855f7', rightColor: '#f59e0b' };
var css =
'.tow{display:block;width:100%;line-height:1.2;color:#fff;--tow-l:' + DEFAULTS.leftColor + ';--tow-r:' + DEFAULTS.rightColor + '}' +
'.tow-head{display:flex;justify-content:space-between;align-items:baseline;gap:1em;margin-bottom:0.45em;font-weight:800}' +
'.tow-side{display:flex;align-items:baseline;gap:0.45em;min-width:0;white-space:nowrap}' +
'.tow-side-r{flex-direction:row-reverse}' +
'.tow-name{font-size:0.85em;letter-spacing:0.06em;text-transform:uppercase;opacity:.85;overflow:hidden;text-overflow:ellipsis}' +
'.tow-pct{font-size:1.35em;font-variant-numeric:tabular-nums}' +
'.tow-side-l .tow-pct{color:color-mix(in srgb,var(--tow-l) 55%,#fff)}' +
'.tow-side-r .tow-pct{color:color-mix(in srgb,var(--tow-r) 55%,#fff)}' +
'.tow-wrap{position:relative;padding:0.45em 0}' +
'.tow-bar{position:relative;height:1.4em;border-radius:999px;overflow:hidden;background:var(--tow-r);box-shadow:inset 0 0 0 1px rgba(255,255,255,.15),0 0.3em 1em rgba(0,0,0,.35)}' +
'.tow-l,.tow-r{position:absolute;top:0;bottom:0;background-size:1.6em 100%}' +
'.tow-l{left:0;width:50%;background-color:var(--tow-l);background-image:repeating-linear-gradient(115deg,rgba(255,255,255,.16) 0 0.4em,transparent 0.4em 0.8em);animation:tow-push-l 0.9s linear infinite}' +
'.tow-r{right:0;left:50%;background-color:var(--tow-r);background-image:repeating-linear-gradient(65deg,rgba(255,255,255,.16) 0 0.4em,transparent 0.4em 0.8em);animation:tow-push-r 0.9s linear infinite}' +
'@keyframes tow-push-l{to{background-position:1.6em 0}}' +
'@keyframes tow-push-r{to{background-position:-1.6em 0}}' +
'.tow-still .tow-l,.tow-still .tow-r{animation-play-state:paused}' +
'.tow-bar::after{content:"";position:absolute;inset:0 0 50% 0;background:linear-gradient(rgba(255,255,255,.28),transparent);pointer-events:none}' +
'.tow-front{position:absolute;top:0.2em;bottom:0.2em;left:50%;width:0.3em;margin-left:-0.15em;border-radius:1em;background:#fff;box-shadow:0 0 0.5em #fff,0 0 1.2em rgba(255,255,255,.7)}' +
'.tow-mid{position:absolute;left:50%;top:0;bottom:0;width:2px;margin-left:-1px;background:rgba(255,255,255,.75);border-radius:2px}' +
'.tow-mid::before,.tow-mid::after{content:"";position:absolute;left:50%;width:0;height:0;margin-left:-0.3em;border:0.3em solid transparent}' +
'.tow-mid::before{top:-0.1em;border-top-color:#fff}' +
'.tow-mid::after{bottom:-0.1em;border-bottom-color:#fff}' +
'.tow-spark{position:absolute;top:50%;width:0.3em;height:0.3em;margin:-0.15em;border-radius:50%;background:#fff;opacity:0;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function clamp(v) { return Math.max(0, Math.min(100, v)); }
function setup(el) {
var d = el.dataset;
var v = clamp(d.value !== undefined && d.value !== '' ? +d.value : DEFAULTS.value);
el.classList.add('tow');
el.style.setProperty('--tow-l', d.leftColor || DEFAULTS.leftColor);
el.style.setProperty('--tow-r', d.rightColor || DEFAULTS.rightColor);
el.setAttribute('role', 'meter');
el.setAttribute('aria-valuemin', '0');
el.setAttribute('aria-valuemax', '100');
el.innerHTML =
'<div class="tow-head" aria-hidden="true">' +
'<span class="tow-side tow-side-l"><span class="tow-pct"></span><span class="tow-name"></span></span>' +
'<span class="tow-side tow-side-r"><span class="tow-pct"></span><span class="tow-name"></span></span>' +
'</div>' +
'<div class="tow-wrap" aria-hidden="true"><div class="tow-bar"><span class="tow-l"></span><span class="tow-r"></span><span class="tow-front"></span></div><span class="tow-mid"></span></div>';
var names = el.querySelectorAll('.tow-name'), pcts = el.querySelectorAll('.tow-pct');
names[0].textContent = d.left || '';
names[1].textContent = d.right || '';
var s = {
v: { value: v }, target: v,
l: el.querySelector('.tow-l'), r: el.querySelector('.tow-r'),
front: el.querySelector('.tow-front'), wrap: el.querySelector('.tow-wrap'),
pl: pcts[0], pr: pcts[1], sparks: []
};
for (var i = 0; i < 8; i++) {
var sp = document.createElement('span');
sp.className = 'tow-spark';
s.wrap.appendChild(sp);
s.sparks.push(sp);
}
el._tow = s;
render(el);
if (reduced()) el.classList.add('tow-still');
}
function render(el) {
var s = el._tow, v = clamp(s.v.value);
s.l.style.width = v + '%';
s.r.style.left = v + '%';
s.front.style.left = v + '%';
var shown = Math.round(clamp(s.target));
s.pl.textContent = Math.round(v) + '%';
s.pr.textContent = (100 - Math.round(v)) + '%';
el.setAttribute('aria-valuenow', String(shown));
var d = el.dataset;
el.setAttribute('aria-label', (d.left || 'Left') + ' ' + shown + '%, ' + (d.right || 'Right') + ' ' + (100 - shown) + '%');
}
function push(el, to) {
var s = el._tow;
to = clamp(to);
var from = s.v.value;
s.target = to;
if (reduced()) { s.v.value = to; render(el); return; }
if (s.anim) s.anim.pause();
var big = Math.abs(to - from);
s.anim = anime.animate(s.v, {
value: to,
duration: 900 + big * 15,
ease: 'outElastic(1, .55)',
onUpdate: function () { render(el); }
});
// Sparks fly back from the front, towards the side that is losing ground.
var dir = to > from ? 1 : -1;
s.sparks.forEach(function (sp, i) {
anime.utils.set(sp, { left: to + '%', x: 0, y: 0, opacity: 0, scale: 1 });
anime.animate(sp, {
x: (dir * anime.utils.random(8, 30) / 10) + 'em',
y: (anime.utils.random(-14, 14) / 10) + 'em',
scale: [1.2, 0],
opacity: [{ to: 1, duration: 40 }, { to: 0, duration: 460 }],
duration: 500,
delay: 150 + i * 12,
ease: 'out(3)'
});
});
anime.animate(s.front, { scaleX: [2.4, 1], duration: 500, ease: 'out(3)' });
}
// Autoplay: a random walk with the odd big shove, staying between 15 and 85.
function auto(el) {
var s = el._tow;
clearTimeout(s.timer);
if (!s.visible) return;
var v = s.target;
var step = Math.random() < 0.25 ? anime.utils.random(14, 24) : anime.utils.random(4, 10);
var dir = v > 68 ? -1 : v < 32 ? 1 : (Math.random() < 0.5 ? -1 : 1);
push(el, Math.max(15, Math.min(85, v + dir * step)));
s.timer = setTimeout(function () { auto(el); }, anime.utils.random(1300, 2100));
}
window.animacious = window.animacious || {};
window.animacious.tugOfWarBar = function (el, value) {
if (!el || !el._tow) return;
push(el, +value);
};
function init() {
// Stripes and autoplay stop while the bar is off screen.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._tow;
s.visible = e.isIntersecting;
el.classList.toggle('tow-still', !e.isIntersecting);
if (!e.isIntersecting) { clearTimeout(s.timer); return; }
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false') {
s.timer = setTimeout(function () { auto(el); }, 700);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-tug-of-war-bar]').forEach(function (el) {
if (el._tow) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-tug-of-war-bar data-left="Cats" data-right="Dogs"
data-value="50"></div>
<script>
// when the votes change:
animacious.tugOfWarBar(document.querySelector('[data-tug-of-war-bar]'), 58);
</script>