Checkbox Tick Draw
Tick a box and it gives a little squeeze, fills with colour, and the tick draws itself in like a pen stroke. Add data-strike and a line crosses the item off too. The real checkboxes stay in the page, so forms and keyboards work as normal.
How to use it
- Paste the snippet just before
</head>. - Add
data-checkbox-tick-drawto a<label>that wraps a checkbox, or to a list of them.
Options
Add any of these attributes to the same element.
data-strike | Draw a line through the label when ticked, like a to-do list. |
data-color | Box colour when ticked. Default #6a0dad. |
data-autoplay | It ticks the boxes one by one and clears them, over and over, while in view. Handy for demos. |
Size it with font-size. Custom elements with role="checkbox" and aria-checked work too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Checkbox Tick Draw: tick a box and it gives a little squeeze, fills with
// colour, and the tick draws itself in with a pen stroke. Untick it and the
// tick wipes away. With data-strike, a line also draws through the label,
// like crossing something off a to-do list. Real checkboxes stay in the
// page (just out of sight), so forms, keyboards and screen readers work as
// normal.
// Usage: <label data-checkbox-tick-draw><input type="checkbox"> Buy milk</label>
// Or put it on a list: every checkbox inside gets one. Elements with
// role="checkbox" (and aria-checked) work too.
// Optional: data-color (box colour when ticked, default #6a0dad), data-strike
// (strike through the label when ticked), data-autoplay (it ticks the boxes
// one by one and clears them again, over and over, while in view; handy for
// demos). Size it with font-size.
var DEFAULTS = { color: '#6a0dad', every: 800 };
var css =
'.ctd{display:flex;align-items:center;gap:0.6em;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.ctd-input{position:absolute;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}' +
'.ctd-box{position:relative;flex:none;display:grid;place-items:center;width:1.15em;height:1.15em;border-radius:0.3em;' +
'box-sizing:border-box;border:0.11em solid #c9c3d6;background:#fff}' +
'.ctd-fill{position:absolute;inset:-1px;border-radius:inherit;background:var(--ctd-color);transform:scale(0)}' +
'.ctd-box svg{position:relative;width:78%;height:78%;overflow:visible}' +
'.ctd-input:focus-visible + .ctd-box, .ctd[role=checkbox]:focus-visible .ctd-box{outline:2px solid var(--ctd-color);outline-offset:2px}' +
'.ctd[role=checkbox]:focus-visible{outline:none}' +
'.ctd-text{position:relative}' +
'.ctd-line{position:absolute;left:-0.1em;right:-0.1em;top:55%;height:0.09em;border-radius:0.05em;background:currentColor;transform:scaleX(0);transform-origin:0 50%;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var reduced = function () { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; };
function build(root, input, el) {
var color = el.dataset.color || DEFAULTS.color;
root.classList.add('ctd');
root.style.setProperty('--ctd-color', color);
var box = document.createElement('span');
box.className = 'ctd-box';
box.setAttribute('aria-hidden', 'true');
box.innerHTML = '<span class="ctd-fill"></span><svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
if (input) {
input.classList.add('ctd-input');
input.after(box);
} else root.insertBefore(box, root.firstChild);
// Gather the label text into one box so a line can strike through it.
var text = null, line = null;
if (el.dataset.strike !== undefined && el.dataset.strike !== 'false') {
text = document.createElement('span');
text.className = 'ctd-text';
while (box.nextSibling) text.appendChild(box.nextSibling);
line = document.createElement('span');
line.className = 'ctd-line';
line.setAttribute('aria-hidden', 'true');
text.appendChild(line);
root.appendChild(text);
}
var item = {
root: root, input: input, box: box, fill: box.firstChild, text: text, line: line, color: color,
tick: anime.svg.createDrawable(box.querySelector('path')),
checked: input ? input.checked : root.getAttribute('aria-checked') === 'true'
};
show(item, item.checked, true);
return item;
}
function show(item, on, instant) {
item.checked = on;
if (item.input) item.input.checked = on;
else item.root.setAttribute('aria-checked', on);
var box = { borderColor: on ? item.color : '#c9c3d6' };
if (instant || reduced()) {
anime.utils.set(item.box, box);
anime.utils.set(item.fill, { scale: on ? 1 : 0 });
anime.utils.set(item.tick, { draw: on ? '0 1' : '0 0' });
if (item.line) {
anime.utils.set(item.line, { scaleX: on ? 1 : 0 });
anime.utils.set(item.text, { opacity: on ? 0.55 : 1 });
}
return;
}
if (on) {
anime.animate(item.box, { scale: [{ to: 0.8, duration: 100, ease: 'out(2)' }, { to: 1, duration: 600, ease: 'outElastic(1, .5)' }] });
anime.animate(item.box, Object.assign({ duration: 150 }, box));
anime.animate(item.fill, { scale: [0, 1], duration: 300, ease: 'out(3)' });
anime.animate(item.tick, { draw: ['0 0', '0 1'], duration: 380, delay: 130, ease: 'out(3)' });
if (item.line) {
anime.utils.set(item.line, { transformOrigin: '0% 50%' });
anime.animate(item.line, { scaleX: [0, 1], duration: 450, delay: 220, ease: 'inOut(2)' });
anime.animate(item.text, { opacity: 0.55, duration: 400, delay: 300 });
}
} else {
// The tick wipes away in the direction it was drawn.
anime.animate(item.tick, { draw: '1 1', duration: 220, ease: 'in(2)' });
anime.animate(item.fill, { scale: 0, duration: 260, delay: 100, ease: 'in(2)' });
anime.animate(item.box, Object.assign({ duration: 200, delay: 200 }, box));
if (item.line) {
anime.utils.set(item.line, { transformOrigin: '100% 50%' });
anime.animate(item.line, { scaleX: 0, duration: 350, ease: 'inOut(2)' });
anime.animate(item.text, { opacity: 1, duration: 300 });
}
}
}
function setup(el) {
var items = [];
var inputs = el.matches('input[type=checkbox]') ? [el] : Array.from(el.querySelectorAll('input[type=checkbox]'));
if (inputs.length) {
inputs.forEach(function (input) {
var item = build(input.closest('label') || input.parentNode, input, el);
input.addEventListener('change', function () { show(item, input.checked); });
items.push(item);
});
} else {
var boxes = el.matches('[role=checkbox]') ? [el] : Array.from(el.querySelectorAll('[role=checkbox]'));
boxes.forEach(function (root) {
var item = build(root, null, el);
// Keyboard-reachable, unless it's a decorative copy hidden from everyone.
if (!root.closest('[aria-hidden="true"]') && !root.hasAttribute('tabindex')) root.tabIndex = 0;
root.addEventListener('click', function () { show(item, !item.checked); });
root.addEventListener('keydown', function (e) {
if (e.key !== ' ') return;
e.preventDefault();
show(item, !item.checked);
});
items.push(item);
});
}
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced() && items.length) {
// Tick each box in turn, pause, clear them all, pause, repeat.
var step = 0;
var timer = null;
var next = function () {
if (!el.matches(':hover') && !el.contains(document.activeElement)) {
var phase = step % (items.length + 2);
if (phase < items.length) show(items[phase], true);
else if (phase === items.length + 1) {
items.forEach(function (item, i) { setTimeout(function () { show(item, false); }, i * 90); });
}
step++;
}
timer = setTimeout(next, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(next, 500);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-checkbox-tick-draw]').forEach(function (el) {
if (el._ctd) return;
el._ctd = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><label data-checkbox-tick-draw><input type="checkbox"> Remember me</label>
<!-- a to-do list: -->
<div data-checkbox-tick-draw data-strike>
<label><input type="checkbox"> Buy milk</label>
<label><input type="checkbox"> Walk the dog</label>
</div>