Icon Array
A row of little figures that fill with colour one after another, from the feet up, to show a proportion such as 7 in 10 people. The count underneath ticks up as each one fills.
How to use it
- Paste the snippet just before
</head>. - Add
data-icon-array="7"to an empty element. It fills 7 of 10 icons; the value can have a fraction, so 7.5 fills half of the eighth.
Options
Add any of these attributes to the same element.
data-total | How many icons. Default 10. |
data-label | Text after “7 in 10”. Default people; leave it empty for none. |
data-icon | person (the default), heart, star or dot. |
data-columns | Icons per row. Default up to 10. |
data-color | Fill colour. Default #a855f7. |
data-caption | false hides the count. |
data-duration | Time to fill every icon. Default 1800 ms. |
data-loop | Empty and fill again every few seconds. |
Size the icons with the --iar-size CSS variable (default 36px); they shrink to fit a narrow screen.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Icon Array: a row (or grid) of little figures that fill with colour one
// after another, rising from the feet up, to show a proportion such as
// "7 in 10 people". The count under them ticks up as each one fills.
// Usage: <div data-icon-array="7" data-total="10" data-label="people"></div>
// The value can have a fraction (7.5 fills half of the eighth icon).
// Optional: data-total (how many icons, default 10), data-label (text
// after "7 in 10", default "people"; empty for none), data-icon ("person",
// the default, "heart", "star" or "dot"), data-columns (icons per row,
// default up to 10), data-color (default #a855f7), data-caption="false"
// (no count), data-duration (ms to fill every icon, default 1800),
// data-loop (empty and fill again every few seconds).
var DEFAULTS = { total: 10, label: 'people', color: '#a855f7', duration: 1800, hold: 2600 };
var ICONS = {
person: '<circle cx="12" cy="6.2" r="4.2"/><path d="M12 12.4c4.6 0 8 2.9 8 6.4v1.7c0 1-.8 1.5-1.7 1.5H5.7c-.9 0-1.7-.5-1.7-1.5v-1.7c0-3.5 3.4-6.4 8-6.4z"/>',
heart: '<path d="M12 21.2s-7.6-4.7-9.7-9.4C.8 8.4 2.9 4.4 6.7 4.4c2.2 0 3.7 1.2 5.3 3.1 1.6-1.9 3.1-3.1 5.3-3.1 3.8 0 5.9 4 4.4 7.4C19.6 16.5 12 21.2 12 21.2z"/>',
star: '<path d="M12 2.4l2.9 6.2 6.7.8-5 4.6 1.4 6.7L12 17.4l-6 3.3 1.4-6.7-5-4.6 6.7-.8z"/>',
dot: '<circle cx="12" cy="12" r="9.5"/>'
};
var css =
'.iar{display:flex;flex-direction:column;align-items:center;gap:12px;font-variant-numeric:tabular-nums}' +
'.iar-grid{display:grid;grid-template-columns:repeat(var(--iar-cols),minmax(0,var(--iar-size,36px)));gap:var(--iar-gap,6px);justify-content:center;width:100%}' +
'.iar-icon{position:relative;display:block;aspect-ratio:1}' +
'.iar-icon svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}' +
'.iar-base{fill:currentColor;opacity:.16}' +
'.iar-fill{fill:var(--iar-c)}' +
'.iar-full svg{animation:iar-pop .4s ease-out}' +
'@keyframes iar-pop{40%{transform:scale(1.18)}}' +
'.iar-cap{font-size:var(--iar-text,15px);font-weight:600;line-height:1.2;text-align:center}' +
'.iar-cap b{font-size:1.7em;font-weight:800;color:var(--iar-c);vertical-align:-.08em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
function setup(el) {
var d = el.dataset;
var total = Math.max(1, Math.round(+d.total || DEFAULTS.total));
var value = Math.max(0, Math.min(total, +el.getAttribute('data-icon-array') || 0));
var label = d.label !== undefined ? d.label : DEFAULTS.label;
var shape = ICONS[d.icon] || ICONS.person;
var cols = Math.max(1, +d.columns || Math.min(total, 10));
var color = d.color || DEFAULTS.color;
var text = function (v) { return (v % 1 ? v.toFixed(1) : v) + ''; };
el.classList.add('iar');
el.style.setProperty('--iar-c', color);
el.style.setProperty('--iar-cols', cols);
el.setAttribute('role', 'img');
el.setAttribute('aria-label', text(value) + ' in ' + total + (label ? ' ' + label : ''));
var html = '<div class="iar-grid" aria-hidden="true">';
for (var i = 0; i < total; i++) {
html += '<span class="iar-icon"><svg viewBox="0 0 24 24"><g class="iar-base">' + shape + '</g>' +
'<g class="iar-fill" style="clip-path:inset(100% 0 0 0)">' + shape + '</g></svg></span>';
}
html += '</div>';
if (d.caption !== 'false') {
html += '<p class="iar-cap" aria-hidden="true" style="margin:0"><b>0</b> in ' + total + (label ? ' ' + esc(label) : '') + '</p>';
}
el.innerHTML = html;
var c = { el: el, value: value, text: text };
c.icons = Array.prototype.slice.call(el.querySelectorAll('.iar-icon'));
c.fills = c.icons.map(function (ic) { return ic.querySelector('.iar-fill'); });
c.num = el.querySelector('.iar-cap b');
el._iar = c;
return c;
}
// Fill to p icons (0 to value): each fills from the bottom up, then the next starts.
function render(c, p) {
c.fills.forEach(function (f, i) {
var k = Math.max(0, Math.min(1, p - i));
f.style.clipPath = 'inset(' + ((1 - k) * 100).toFixed(1) + '% 0 0 0)';
var full = k >= 1;
if (full !== c.icons[i].classList.contains('iar-full')) c.icons[i].classList.toggle('iar-full', full);
});
if (c.num) c.num.textContent = p >= c.value ? c.text(c.value) : Math.floor(p);
}
function play(c) {
var d = c.el.dataset;
var duration = +d.duration || DEFAULTS.duration;
var st = { p: 0 };
render(c, 0);
anime.animate(st, {
p: c.value, duration: duration * Math.max(0.3, c.value / (c.fills.length || 1)), ease: 'inOut(1.4)',
onUpdate: function () { render(c, st.p); },
onComplete: function () {
render(c, c.value);
if (d.loop === undefined || d.loop === 'false') return;
setTimeout(function () {
var o = { p: c.value };
anime.animate(o, {
p: 0, duration: 600, ease: 'in(2)',
onUpdate: function () { render(c, o.p); },
onComplete: function () { setTimeout(function () { play(c); }, 350); }
});
}, 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._iar);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-icon-array]').forEach(function (el) {
if (el._iar) return;
var c = setup(el);
if (reduced()) { render(c, c.value); c.icons.forEach(function (i) { i.classList.remove('iar-full'); }); return; }
render(c, 0);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-icon-array="7" data-label="people"></div>
<!-- or, 25 in 100 as hearts: -->
<div data-icon-array="25" data-total="100" data-icon="heart"></div>