Calendar Events Drop
Events fall into a weekly calendar one after another. Each one tumbles in from the top, straightens up as it drops, lands in its slot with a little squash and a flash of colour, and the week fills up in front of you. A red line marks the time now.
Standup
Roadmap
1:1 with Sam
Focus time
Standup
Lunch & learn
Design crit
Workshop
Hiring
Standup
Demo day
Retro
How to use it
- Paste the snippet just before
</head>. - Add
data-calendar-events-dropto a container, with one child per event. - Give each event
data-day(1 for the first column) anddata-timeasstart-endin 24-hour time, such as9:30-11. Its text is the title.
Options
Add any of these attributes to the same element.
data-day / data-time | On an event: its column, and when it starts and ends. |
data-days | Column names. Default Mon,Tue,Wed,Thu,Fri. |
data-hours | First and last hour shown. Default 9-17. |
data-color | Event colour, on the container or on a single event. Default #7c3aed. |
data-today | Column to mark as today, counting from 1. |
data-now | With data-today: draw a “now” line at this time, such as 11:20. |
data-stagger | Gap between events dropping. Default 260 ms. |
data-loop | Lift the events off and drop them again. |
Events of an hour or more also show their times. Text size follows the container’s font-size; each hour is 2.3em tall.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Calendar Events Drop: events fall into a weekly calendar one after
// another, landing in their slots with a little squash and a flash.
// Usage:
// <div data-calendar-events-drop data-hours="9-17">
// <span data-day="1" data-time="9:30-10:30">Team standup</span>
// <span data-day="3" data-time="13-15" data-color="#db2777">Design review</span>
// </div>
// Each event is a child with data-day (1 for the first column) and
// data-time ("start-end" in 24-hour time, as 9, 9:30 or 9.5); its text is
// the title.
// Optional: data-days (column names, default Mon,Tue,Wed,Thu,Fri),
// data-hours (first and last hour shown, default 9-17), data-color (event
// colour, default #7c3aed; also on a single event), data-now (draw a
// "now" line at this time, e.g. 11:15, on data-today's column),
// data-today (column to mark as today, 1-based), data-stagger (ms between
// events, default 260), data-loop (lift the events off and drop them
// again).
var DEFAULTS = { days: 'Mon,Tue,Wed,Thu,Fri', hours: '9-17', color: '#7c3aed', stagger: 260 };
var css =
'.ced{display:grid;grid-template-columns:2.8em minmax(0,1fr);width:100%;line-height:1.2;background:#fff;border-radius:.8em;box-shadow:0 .3em 1.2em rgba(40,20,80,.1);padding:.6em .6em .6em 0;box-sizing:border-box}' +
'.ced-head{grid-column:2;display:grid;border-bottom:1px solid #e9e4f2;padding-bottom:.35em}' +
'.ced-head span{text-align:center;font-weight:700;font-size:.8em;color:#7a7090;letter-spacing:.02em}' +
'.ced-head span.ced-today{color:var(--ced-c)}' +
'.ced-head span.ced-today::after{content:"";display:block;width:.35em;height:.35em;margin:.2em auto 0;border-radius:50%;background:var(--ced-c)}' +
'.ced-hrs{position:relative}' +
'.ced-hrs span{position:absolute;right:.6em;translate:0 -50%;font-size:.68em;color:#a49bb6;font-variant-numeric:tabular-nums;white-space:nowrap}' +
'.ced-body{position:relative;overflow:hidden;background-image:linear-gradient(#efebf6 1px,transparent 1px);border-left:1px solid #efebf6}' +
'.ced-col{position:absolute;top:0;bottom:0;border-right:1px solid #efebf6}' +
'.ced-col.ced-today{background:color-mix(in srgb,var(--ced-c) 5%,transparent)}' +
'.ced-ev{position:absolute;box-sizing:border-box;padding:.25em .35em .2em .45em;border-radius:.35em;border-left:.25em solid var(--ced-e);background:color-mix(in srgb,var(--ced-e) 16%,#fff);overflow:hidden;opacity:0;transform-origin:50% 100%;' +
'box-shadow:0 .1em .3em color-mix(in srgb,var(--ced-e) 25%,transparent)}' +
'.ced-ev b{display:block;font-size:.72em;font-weight:700;color:color-mix(in srgb,var(--ced-e) 70%,#1c1230);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.ced-ev i{display:block;font-style:normal;font-size:.62em;color:color-mix(in srgb,var(--ced-e) 55%,#4b4060);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.ced-flash{position:absolute;border-radius:.35em;background:var(--ced-e);opacity:0;pointer-events:none}' +
'.ced-now{position:absolute;height:2px;margin-top:-1px;background:#ef4444;transform-origin:0 50%}' +
'.ced-now::before{content:"";position:absolute;left:-.3em;top:50%;width:.6em;height:.6em;margin-top:-.3em;border-radius:50%;background:#ef4444}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function hour(t) {
t = String(t).trim();
var m = t.match(/^(\d{1,2})(?::(\d{2}))?$/);
return m ? +m[1] + (m[2] ? +m[2] / 60 : 0) : parseFloat(t);
}
function clock(h) {
var hh = Math.floor(h), mm = Math.round((h - hh) * 60);
return hh + ':' + (mm < 10 ? '0' : '') + mm;
}
function setup(el) {
var d = el.dataset;
var s = (el._ced = { events: [] });
var days = (d.days || DEFAULTS.days).split(',').map(function (x) { return x.trim(); });
var hr = (d.hours || DEFAULTS.hours).split('-').map(hour);
var h0 = hr[0], h1 = Math.max(h0 + 1, hr[1] || h0 + 8), span = h1 - h0;
var today = +d.today || 0;
var kids = Array.from(el.children);
el.classList.add('ced');
el.style.setProperty('--ced-c', d.color || DEFAULTS.color);
var cols = 'repeat(' + days.length + ',minmax(0,1fr))';
var head = document.createElement('div');
head.className = 'ced-head';
head.style.gridTemplateColumns = cols;
days.forEach(function (name, i) {
var c = document.createElement('span');
c.textContent = name;
if (i + 1 === today) c.className = 'ced-today';
head.appendChild(c);
});
var hrs = document.createElement('div');
hrs.className = 'ced-hrs';
hrs.setAttribute('aria-hidden', 'true');
var body = document.createElement('div');
body.className = 'ced-body';
body.style.height = span * 2.3 + 'em';
body.style.backgroundSize = '100% ' + 100 / span + '%';
for (var h = Math.ceil(h0); h < h1; h++) {
var lab = document.createElement('span');
lab.style.top = ((h - h0) / span) * 100 + '%';
lab.textContent = h + ':00';
if (h > h0) hrs.appendChild(lab);
}
days.forEach(function (name, i) {
var col = document.createElement('div');
col.className = 'ced-col' + (i + 1 === today ? ' ced-today' : '');
col.style.left = (i / days.length) * 100 + '%';
col.style.width = 100 / days.length + '%';
body.appendChild(col);
});
el.appendChild(document.createElement('span'));
el.appendChild(head);
el.appendChild(hrs);
el.appendChild(body);
kids.forEach(function (k) {
var day = +k.getAttribute('data-day') || 1;
var t = (k.getAttribute('data-time') || '').split('-').map(hour);
var a = isNaN(t[0]) ? h0 : t[0], b = isNaN(t[1]) ? a + 1 : t[1];
var title = k.textContent.trim();
var ev = document.createElement('div');
ev.className = 'ced-ev';
ev.style.setProperty('--ced-e', k.getAttribute('data-color') || d.color || DEFAULTS.color);
ev.style.left = 'calc(' + ((day - 1) / days.length) * 100 + '% + 2px)';
ev.style.width = 'calc(' + 100 / days.length + '% - 4px)';
ev.style.top = 'calc(' + ((a - h0) / span) * 100 + '% + 1px)';
ev.style.height = 'calc(' + ((b - a) / span) * 100 + '% - 2px)';
ev.innerHTML = '<b></b>' + (b - a >= 1 ? '<i></i>' : '');
ev.firstChild.textContent = title;
if (b - a >= 1) ev.lastChild.textContent = clock(a) + '–' + clock(b);
ev.setAttribute('aria-label', title + ', ' + (days[day - 1] || '') + ' ' + clock(a) + ' to ' + clock(b));
ev.setAttribute('role', 'note');
var flash = document.createElement('div');
flash.className = 'ced-flash';
flash.setAttribute('aria-hidden', 'true');
['left', 'width', 'top', 'height'].forEach(function (p) { flash.style[p] = ev.style[p]; });
flash.style.setProperty('--ced-e', ev.style.getPropertyValue('--ced-e'));
body.appendChild(flash);
body.appendChild(ev);
k.remove();
s.events.push({ el: ev, flash: flash });
});
if (d.now && today) {
var n = hour(d.now);
if (n >= h0 && n <= h1) {
s.now = document.createElement('div');
s.now.className = 'ced-now';
s.now.setAttribute('aria-hidden', 'true');
s.now.style.top = ((n - h0) / span) * 100 + '%';
s.now.style.left = ((today - 1) / days.length) * 100 + '%';
s.now.style.width = 100 / days.length + '%';
body.appendChild(s.now);
}
}
s.body = body;
}
function play(el) {
var s = el._ced, stagger = +el.dataset.stagger || DEFAULTS.stagger, last = 0;
s.events.forEach(function (e, i) {
var fall = e.el.offsetTop + e.el.offsetHeight + 12, delay = 300 + i * stagger;
anime.utils.set(e.el, { opacity: 1, y: -fall, scaleX: 1, scaleY: 1, rotate: (Math.random() - 0.5) * 8 });
var tl = anime.createTimeline({ delay: delay });
tl.add(e.el, { y: [-fall, 0], rotate: 0, duration: 420 + fall * 0.6, ease: 'in(2.2)' }, 0)
.add(e.el, { scaleY: [1, 0.86, 1.04, 1], scaleX: [1, 1.05, 0.99, 1], duration: 460, ease: 'out(2)' }, '<')
.add(e.flash, { opacity: [0.45, 0], scale: [1, 1.12], duration: 600, ease: 'out(3)' }, '<<');
last = Math.max(last, delay + 420 + fall * 0.6 + 460);
});
if (s.now) anime.animate(s.now, { scaleX: [0, 1], opacity: [0, 1], duration: 600, delay: 200, ease: 'out(3)' });
if (el.dataset.loop === undefined) return;
setTimeout(function () {
s.events.forEach(function (e, i) {
anime.animate(e.el, { y: -(e.el.offsetTop + e.el.offsetHeight + 12), opacity: [1, 0.4], duration: 420, delay: i * 50, ease: 'in(3)' });
});
setTimeout(function () { play(el); }, 420 + s.events.length * 50 + 300);
}, last + 2800);
}
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-calendar-events-drop]').forEach(function (el) {
if (el._ced) return;
setup(el);
if (reduced()) el._ced.events.forEach(function (e) { e.el.style.opacity = 1; });
else io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-calendar-events-drop data-hours="9-17" data-today="3" data-now="11:20">
<span data-day="1" data-time="9-10">Standup</span>
<span data-day="2" data-time="10-11:30" data-color="#db2777">1:1 with Sam</span>
<span data-day="4" data-time="13-15" data-color="#0ea5e9">Workshop</span>
</div>