Comparison Table Ticks
The ticks and crosses in a comparison table draw themselves in, one column at a time and top to bottom: each tick’s green disc pops and its check is drawn in a stroke, crosses sketch in more quietly in grey. The column you want to sell gets a tint as its turn comes. Write the table as normal HTML with “yes” and “no” in the cells.
| Free | Pro | Team | |
|---|---|---|---|
| Projects | 3 | Unlimited | Unlimited |
| Custom domain | no | yes | yes |
| Analytics | no | yes | yes |
| Version history | yes | yes | yes |
| Team roles | no | no | yes |
| Priority support | no | no | yes |
How to use it
- Paste the snippet just before
</head>. - Add
data-comparison-table-ticksto a<table>, with the column names in<thead>and a<th>naming each row. - Write
yesorno(or ✓ and ✗) in the cells that should become ticks and crosses. Other values, such as50 GB, stay as text and fade in with their column.
Options
Add any of these attributes to the same element.
data-highlight | Column to pick out with a tint, counting the first value column as 1. |
data-color | Colour of the ticks. Default #16a34a. |
data-accent | Colour of the highlighted column. Default #7c3aed. |
data-column | Time between one column starting and the next. Default 520 ms. |
data-stagger | Time between cells down a column. Default 90 ms. |
data-loop | Clear the marks and draw them again. |
Screen readers still hear “Yes” and “No” in each cell. The table fills its container’s width and takes its text size from it; the marks are 1.45em across.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Comparison Table Ticks: the ticks and crosses in a comparison table draw
// themselves in, column by column, top to bottom. Any other values in the
// table fade in with their column.
// Usage:
// <table data-comparison-table-ticks>
// <thead><tr><th></th><th>Free</th><th>Pro</th></tr></thead>
// <tbody>
// <tr><th>Custom domain</th><td>no</td><td>yes</td></tr>
// <tr><th>Storage</th><td>1 GB</td><td>50 GB</td></tr>
// </tbody>
// </table>
// Cells reading yes, y, true, ✓ or ✔ become ticks; no, n, false, ✗, ✕
// or - become crosses. Screen readers still hear "Yes" or "No".
// Optional: data-highlight (column to pick out, counting the first value
// column as 1), data-color (tick colour, default #16a34a), data-accent
// (highlight colour, default #7c3aed), data-stagger (ms between cells,
// default 90), data-column (ms between columns, default 520), data-loop
// (clear the marks and draw them again).
var DEFAULTS = { color: '#16a34a', accent: '#7c3aed', stagger: 90, column: 520 };
var YES = /^(yes|y|true|✓|✔|✔️|☑)$/i, NO = /^(no|n|false|✗|✕|✘|×|-|–|—)$/i;
var css =
'.ctt{border-collapse:separate;border-spacing:0;width:100%;line-height:1.25;font-size:inherit;text-align:center}' +
'.ctt th,.ctt td{padding:.6em .5em;border-bottom:1px solid #ece7f4;vertical-align:middle}' +
'.ctt tbody tr:last-child>*{border-bottom:0}' +
'.ctt tbody th{text-align:left;font-weight:600;color:#3b3150}' +
'.ctt thead th{font-weight:800;color:#3b3150;padding-top:.8em}' +
'.ctt td{color:#4b4060;font-weight:600}' +
'.ctt .ctt-hl{background:color-mix(in srgb,var(--ctt-a) 8%,transparent)}' +
'.ctt thead .ctt-hl{border-radius:.7em .7em 0 0;color:var(--ctt-a)}' +
'.ctt tbody tr:last-child .ctt-hl{border-radius:0 0 .7em .7em}' +
'.ctt-mark{display:inline-block;width:1.45em;height:1.45em;vertical-align:middle}' +
'.ctt-mark circle{transform-box:fill-box;transform-origin:center}' +
'.ctt-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function mark(yes, color) {
return yes
? '<svg class="ctt-mark" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="' + color + '" fill-opacity=".15"/>' +
'<path d="M7 12.5l3.3 3.3L17.2 8.8" fill="none" stroke="' + color + '" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" pathLength="1" stroke-dasharray="1 2" stroke-dashoffset="0"/></svg>'
: '<svg class="ctt-mark" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#ece9f1"/>' +
'<path d="M8.6 8.6l6.8 6.8M15.4 8.6l-6.8 6.8" fill="none" stroke="#a39cb0" stroke-width="2.3" stroke-linecap="round" pathLength="1" stroke-dasharray="1 2" stroke-dashoffset="0"/></svg>';
}
function setup(el) {
var d = el.dataset;
var s = (el._ctt = { cols: [] });
var color = d.color || DEFAULTS.color;
el.classList.add('ctt');
el.style.setProperty('--ctt-a', d.accent || DEFAULTS.accent);
var hl = +d.highlight || 0;
Array.from(el.rows).forEach(function (row) {
var first = row.cells[0] && row.cells[0].tagName === 'TH' && row.parentNode.tagName !== 'THEAD';
var head = row.parentNode.tagName === 'THEAD';
Array.from(row.cells).forEach(function (cell, i) {
// Column number among the value columns (row labels don't count).
var c = (head || first) ? i : i + 1;
if (c < 1) return;
var col = s.cols[c] = s.cols[c] || { head: null, cells: [] };
if (c === hl) cell.classList.add('ctt-hl');
if (head) { col.head = cell; return; }
var txt = cell.textContent.trim(), item = { cell: cell };
if (YES.test(txt) || NO.test(txt)) {
var yes = YES.test(txt);
cell.innerHTML = mark(yes, color) + '<span class="ctt-sr">' + (yes ? 'Yes' : 'No') + '</span>';
item.svg = cell.firstChild;
item.circle = item.svg.querySelector('circle');
item.path = item.svg.querySelector('path');
item.yes = yes;
}
col.cells.push(item);
});
});
s.cols = s.cols.filter(Boolean);
s.hl = el.querySelectorAll('.ctt-hl');
}
function clear(el) {
var s = el._ctt;
s.cols.forEach(function (col) {
col.cells.forEach(function (it) {
if (it.path) {
it.path.setAttribute('stroke-dashoffset', 1.01);
anime.utils.set(it.circle, { scale: 0 });
} else it.cell.style.opacity = 0;
});
if (col.head) col.head.style.opacity = 0.35;
});
s.hl.forEach(function (c) { c.style.backgroundColor = 'transparent'; });
}
function play(el) {
var s = el._ctt, d = el.dataset;
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var gap = d.column !== undefined ? +d.column : DEFAULTS.column;
clear(el);
var end = 0;
s.cols.forEach(function (col, ci) {
var t0 = 250 + ci * gap;
if (col.head) {
anime.animate(col.head, { opacity: [0.35, 1], scale: [0.92, 1], duration: 380, delay: t0, ease: 'outBack(2)' });
if (col.head.classList.contains('ctt-hl')) {
var cells = [col.head].concat(col.cells.map(function (it) { return it.cell; }));
setTimeout(function () { cells.forEach(function (c) { c.style.transition = 'background-color .5s'; c.style.backgroundColor = ''; }); }, t0);
}
}
col.cells.forEach(function (it, ri) {
var t = t0 + 120 + ri * stagger;
if (it.path) {
anime.animate(it.circle, { scale: [0, 1.15, 1], duration: 420, delay: t, ease: 'out(3)' });
anime.animate(it.path, { strokeDashoffset: [1.01, 0], duration: it.yes ? 380 : 340, delay: t + 120, ease: 'inOut(2)' });
} else {
anime.animate(it.cell, { opacity: [0, 1], duration: 380, delay: t, ease: 'out(2)' });
}
end = Math.max(end, t + 520);
});
});
if (d.loop === undefined) return;
setTimeout(function () {
s.hl.forEach(function (c) { c.style.backgroundColor = 'transparent'; });
anime.animate(el.querySelectorAll('.ctt-mark'), { opacity: [1, 0], duration: 350, ease: 'in(2)', onComplete: function () {
el.querySelectorAll('.ctt-mark').forEach(function (m) { m.style.opacity = ''; });
setTimeout(function () { play(el); }, 250);
} });
}, end + 2600);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-comparison-table-ticks]').forEach(function (el) {
if (el._ctt || el.tagName !== 'TABLE') return;
setup(el);
if (!reduced()) {
clear(el);
io.observe(el);
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><table data-comparison-table-ticks data-highlight="2">
<thead><tr><th></th><th>Free</th><th>Pro</th></tr></thead>
<tbody>
<tr><th>Storage</th><td>1 GB</td><td>50 GB</td></tr>
<tr><th>Custom domain</th><td>no</td><td>yes</td></tr>
<tr><th>Analytics</th><td>no</td><td>yes</td></tr>
</tbody>
</table>