Date Picker Slide
A month calendar whose arrows slide the month out sideways while the next one glides in from the other side, the month name sliding along with it. Pick a day and a filled circle pops in behind it while the old one shrinks away. Today wears a ring.
How to use it
- Paste the snippet just before
</head>. - Add an empty element with
data-date-picker-slideand anaria-label: the calendar is built inside it. - Size it with
font-size: the calendar is about 18em wide.
Options
Add any of these attributes to the same element.
data-value | The chosen date, as YYYY-MM-DD. Default none. |
data-month | The month shown first, as YYYY-MM. Default the chosen date’s month, or this month. |
data-week-start | 1 to start weeks on Monday, 0 for Sunday. Default 1. |
data-locale | Language for month and day names. Default en-GB. |
data-name | Adds a hidden input with this name holding YYYY-MM-DD, so the date posts with a form. |
data-color | Colour of the chosen day. Default #6a0dad. |
data-duration | How long a month takes to slide. Default 420 ms. |
data-autoplay | It flips through months and picks days by itself while in view, and stops as soon as you use it. |
Every day is a button with its full date as its label. Arrow keys move between days (sliding the month when you cross into the next), and Page Up and Page Down change month. Listen for the change event: e.detail.value is YYYY-MM-DD and e.detail.date a Date.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Date Picker Slide: a month calendar. The arrows slide the month out
// sideways while the next one slides in from the other side, and the
// month name rolls along with it. Pick a day and a filled circle pops
// behind it while the old one shrinks away. Today has a ring.
// Usage: <div data-date-picker-slide aria-label="Choose a date"></div>
// Optional: data-value (the chosen date, YYYY-MM-DD), data-month (the
// month shown first, YYYY-MM; default the chosen date's month or this
// month), data-week-start (0 for Sunday, 1 for Monday; default 1),
// data-locale (for month and day names, default en-GB), data-name (adds a
// hidden input with this name holding YYYY-MM-DD, so it posts with a
// form), data-color (default #6a0dad), data-duration (slide time in ms,
// default 420), data-autoplay (it flips months and picks days by itself
// while in view; it stops as soon as you use it).
// Arrow keys move between days, Page Up/Down change month.
// Listen for the "change" event: e.detail.value is YYYY-MM-DD, e.detail.date a Date.
var DEFAULTS = { weekStart: 1, locale: 'en-GB', color: '#6a0dad', duration: 420 };
var PREV = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>';
var NEXT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>';
var css =
'.dps{display:inline-block;box-sizing:border-box;padding:.9em;border-radius:1.1em;background:#fff;color:#18181b;' +
'box-shadow:0 0 0 1px rgba(0,0,0,.07),0 10px 30px rgba(15,15,30,.09);-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}' +
'.dps-head{display:flex;align-items:center;justify-content:space-between;gap:.4em;margin-bottom:.5em}' +
'.dps-title{position:relative;flex:1;height:1.6em;line-height:1.6em;overflow:hidden;text-align:center;font-weight:700;white-space:nowrap}' +
'.dps-title span{position:absolute;left:0;right:0;top:0}' +
'.dps-nav{display:grid;place-items:center;flex:none;width:2em;height:2em;margin:0;padding:0;border:0;border-radius:50%;background:transparent;' +
'color:#52525b;font:inherit;cursor:pointer;transition:background-color .2s,color .2s;touch-action:manipulation}' +
'.dps-nav svg{width:1.1em;height:1.1em}' +
'.dps-nav:hover,.dps-nav:focus-visible,.dps-press{background:var(--dps-soft);color:var(--dps-color);outline:none}' +
'.dps-week,.dps-grid{display:grid;grid-template-columns:repeat(7,2.3em)}' +
'.dps-week span{height:1.8em;line-height:1.8em;text-align:center;font-size:.78em;font-weight:600;color:#8a8a93}' +
'.dps-view{position:relative;overflow:hidden;height:13.8em}' +
'.dps-grid{position:absolute;left:0;top:0;grid-template-rows:repeat(6,2.3em)}' +
'.dps-day{position:relative;display:grid;place-items:center;width:2.3em;height:2.3em;margin:0;padding:0;border:0;background:none;' +
'color:inherit;font:inherit;font-variant-numeric:tabular-nums;cursor:pointer;touch-action:manipulation;outline:none}' +
'.dps-day b{position:relative;font-weight:500;z-index:1;transition:color .2s}' +
'.dps-day i{position:absolute;inset:.18em;border-radius:50%;pointer-events:none}' +
'.dps-day .dps-hover{background:var(--dps-soft);opacity:0;transition:opacity .15s}' +
'.dps-day:hover .dps-hover,.dps-day:focus-visible .dps-hover{opacity:1}' +
'.dps-day:focus-visible .dps-hover{box-shadow:0 0 0 2px var(--dps-color)}' +
'.dps-day .dps-dot{background:var(--dps-color);transform:scale(0)}' +
'.dps-today .dps-hover{opacity:1;background:none;box-shadow:inset 0 0 0 1.5px var(--dps-line)}' +
'.dps-out b{color:#c4c4cc}' +
'.dps-sel b{color:#fff;font-weight:700}' +
'.dps-sel .dps-dot{transform:none}' +
'.dps-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 on(v) { return v !== undefined && v !== 'false'; }
function rgba(hex, a) {
var m = /^#?([0-9a-f]{6})$/i.exec(hex);
if (!m) return 'rgba(106,13,173,' + a + ')';
var n = parseInt(m[1], 16);
return 'rgba(' + (n >> 16) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
}
function pad(n) { return (n < 10 ? '0' : '') + n; }
function iso(dt) { return dt.getFullYear() + '-' + pad(dt.getMonth() + 1) + '-' + pad(dt.getDate()); }
function parse(s) {
var m = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/.exec(s || '');
return m ? new Date(+m[1], +m[2] - 1, m[3] ? +m[3] : 1) : null;
}
function same(a, b) { return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
function setup(el) {
var d = el.dataset;
var mock = !!el.closest('[aria-hidden="true"]');
var color = d.color || DEFAULTS.color;
var locale = d.locale || DEFAULTS.locale;
var weekStart = d.weekStart === '0' ? 0 : DEFAULTS.weekStart;
var duration = parseFloat(d.duration) || DEFAULTS.duration;
var today = new Date();
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
var selected = parse(d.value);
var start = parse(d.month) || selected || today;
var year = start.getFullYear(), month = start.getMonth();
var label = el.getAttribute('aria-label') || 'Choose a date';
var monthFmt, dayFmt, longFmt;
try {
monthFmt = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' });
dayFmt = new Intl.DateTimeFormat(locale, { weekday: 'short' });
longFmt = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
} catch (err) {
monthFmt = new Intl.DateTimeFormat('en-GB', { month: 'long', year: 'numeric' });
dayFmt = new Intl.DateTimeFormat('en-GB', { weekday: 'short' });
longFmt = new Intl.DateTimeFormat('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
el.classList.add('dps');
el.style.setProperty('--dps-color', color);
el.style.setProperty('--dps-soft', rgba(color, 0.1));
el.style.setProperty('--dps-line', rgba(color, 0.45));
el.textContent = '';
var tag = mock ? 'span' : 'button';
var head = document.createElement('div');
head.className = 'dps-head';
var prev = document.createElement(tag), next = document.createElement(tag);
prev.className = next.className = 'dps-nav';
prev.innerHTML = PREV;
next.innerHTML = NEXT;
var title = document.createElement('div');
title.className = 'dps-title';
head.appendChild(prev);
head.appendChild(title);
head.appendChild(next);
var week = document.createElement('div');
week.className = 'dps-week';
week.setAttribute('aria-hidden', 'true');
for (var w = 0; w < 7; w++) {
var wd = document.createElement('span');
// 2 Jan 2023 was a Monday.
wd.textContent = dayFmt.format(new Date(2023, 0, 1 + ((weekStart + w) % 7))).slice(0, 2);
week.appendChild(wd);
}
var view = document.createElement('div');
view.className = 'dps-view';
el.appendChild(head);
el.appendChild(week);
el.appendChild(view);
var hidden = null, live = null;
if (!mock) {
prev.type = next.type = 'button';
prev.setAttribute('aria-label', 'Previous month');
next.setAttribute('aria-label', 'Next month');
el.setAttribute('role', 'group');
el.setAttribute('aria-label', label);
title.setAttribute('aria-live', 'polite');
live = document.createElement('span');
live.className = 'dps-sr';
live.setAttribute('aria-live', 'polite');
el.appendChild(live);
if (d.name) {
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = d.name;
hidden.value = selected ? iso(selected) : '';
el.appendChild(hidden);
}
}
var grid = null, focusDate = selected || today;
function build(y, m) {
var g = document.createElement('div');
g.className = 'dps-grid';
if (!mock) g.setAttribute('role', 'grid');
var first = new Date(y, m, 1);
var offset = (first.getDay() - weekStart + 7) % 7;
var focusIn = focusDate.getFullYear() === y && focusDate.getMonth() === m;
for (var i = 0; i < 42; i++) {
var dt = new Date(y, m, 1 - offset + i);
var b = document.createElement(tag);
b.className = 'dps-day' + (dt.getMonth() !== m ? ' dps-out' : '') + (same(dt, today) ? ' dps-today' : '') + (same(dt, selected) ? ' dps-sel' : '');
b.innerHTML = '<i class="dps-hover"></i><i class="dps-dot"></i><b>' + dt.getDate() + '</b>';
b._dpsDate = dt;
if (!mock) {
b.type = 'button';
b.setAttribute('aria-label', longFmt.format(dt));
b.setAttribute('aria-pressed', same(dt, selected) ? 'true' : 'false');
b.tabIndex = (focusIn ? same(dt, focusDate) : dt.getDate() === 1 && dt.getMonth() === m) ? 0 : -1;
}
g.appendChild(b);
}
return g;
}
function setTitle(dir) {
var span = document.createElement('span');
span.textContent = monthFmt.format(new Date(year, month, 1));
var old = title.firstChild;
title.appendChild(span);
if (!old) return;
if (!dir || reduced()) { old.remove(); return; }
anime.animate(old, { x: ['0%', (dir > 0 ? -60 : 60) + '%'], opacity: [1, 0], duration: duration * 0.8, ease: 'out(3)', onComplete: function () { old.remove(); } });
anime.animate(span, { x: [(dir > 0 ? 60 : -60) + '%', '0%'], opacity: [0, 1], duration: duration, ease: 'out(3)' });
}
function show(y, m, dir) {
var dt = new Date(y, m, 1);
year = dt.getFullYear();
month = dt.getMonth();
var old = grid;
grid = build(year, month);
view.appendChild(grid);
setTitle(dir);
if (!old) return;
if (!dir || reduced()) { old.remove(); return; }
var w = view.offsetWidth;
anime.utils.remove(old);
anime.animate(old, { x: [0, -dir * w], opacity: [1, 0.2], duration: duration, ease: 'inOut(3)', onComplete: function () { old.remove(); } });
anime.animate(grid, { x: [dir * w, 0], duration: duration, ease: 'inOut(3)' });
}
show(year, month, 0);
function cellFor(dt) {
var cells = grid.children;
for (var i = 0; i < cells.length; i++) if (same(cells[i]._dpsDate, dt) && !cells[i].classList.contains('dps-out')) return cells[i];
return null;
}
function pick(dt, quiet) {
if (same(dt, selected)) return;
var oldCell = selected ? cellFor(selected) : null;
if (dt.getMonth() !== month || dt.getFullYear() !== year) show(dt.getFullYear(), dt.getMonth(), dt > new Date(year, month, 1) ? 1 : -1);
selected = dt;
focusDate = dt;
var cell = cellFor(dt);
if (oldCell) {
oldCell.classList.remove('dps-sel');
if (!mock) oldCell.setAttribute('aria-pressed', 'false');
if (!reduced()) anime.animate(oldCell.querySelector('.dps-dot'), { scale: [1, 0], duration: 220, ease: 'in(2)' });
}
if (cell) {
cell.classList.add('dps-sel');
if (!mock) cell.setAttribute('aria-pressed', 'true');
if (!reduced()) {
anime.animate(cell.querySelector('.dps-dot'), { scale: [0, 1], duration: 560, ease: 'outElastic(1, .55)' });
anime.animate(cell.querySelector('b'), { scale: [0.7, 1.15, 1], duration: 420, ease: 'out(3)' });
}
}
if (hidden) hidden.value = iso(dt);
if (live) live.textContent = longFmt.format(dt) + ' selected';
if (!quiet && !mock) el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: iso(dt), date: dt } }));
}
function press(btn) {
btn.classList.add('dps-press');
setTimeout(function () { btn.classList.remove('dps-press'); }, 260);
if (!reduced()) anime.animate(btn, { scale: [0.85, 1], duration: 380, ease: 'outElastic(1, .6)' });
}
if (!mock) {
prev.addEventListener('click', function () { stop(); press(prev); show(year, month - 1, -1); });
next.addEventListener('click', function () { stop(); press(next); show(year, month + 1, 1); });
view.addEventListener('click', function (e) {
var b = e.target.closest('.dps-day');
if (!b) return;
stop();
pick(b._dpsDate);
});
view.addEventListener('keydown', function (e) {
var b = e.target.closest('.dps-day');
if (!b) return;
var dt = b._dpsDate, step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
var target = null;
if (step) target = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + step);
else if (e.key === 'PageUp' || e.key === 'PageDown') target = new Date(dt.getFullYear(), dt.getMonth() + (e.key === 'PageUp' ? -1 : 1), Math.min(dt.getDate(), 28));
else return;
e.preventDefault();
stop();
focusDate = target;
if (target.getMonth() !== month || target.getFullYear() !== year) show(target.getFullYear(), target.getMonth(), target > dt ? 1 : -1);
var c = cellFor(target);
if (c) {
Array.prototype.forEach.call(grid.children, function (x) { x.tabIndex = -1; });
c.tabIndex = 0;
c.focus();
}
});
el.addEventListener('pointerdown', stop);
}
// Autoplay: picks a day, flips forward a month or two, picks again, comes back.
var stopped = false, timer = null, startY = year, startM = month;
function stop() { stopped = true; clearTimeout(timer); }
function later(fn, ms) { timer = setTimeout(function () { if (!stopped) fn(); }, ms); }
var plan = [['pick', 0, 12], ['wait'], ['next'], ['pick', 1, 19], ['wait'], ['next'], ['pick', 2, 6], ['wait'], ['prev'], ['prev'], ['pick', 0, 24], ['wait']];
function run(i) {
var s = plan[i], go = function (ms) { later(function () { run((i + 1) % plan.length); }, ms); };
if (s[0] === 'pick') {
var dt = new Date(startY, startM + s[1], s[2]);
var c = cellFor(dt);
if (c && !reduced()) anime.animate(c, { scale: [0.88, 1], duration: 300, ease: 'out(2)' });
pick(dt, true);
go(900);
} else if (s[0] === 'wait') go(600);
else {
var btn = s[0] === 'next' ? next : prev;
press(btn);
show(year, month + (s[0] === 'next' ? 1 : -1), s[0] === 'next' ? 1 : -1);
go(duration + 500);
}
}
if (on(d.autoplay) && !reduced()) {
new IntersectionObserver(function (entries) {
if (stopped) return;
clearTimeout(timer);
if (!entries[0].isIntersecting) return;
if (year !== startY || month !== startM) show(startY, startM, 0);
later(function () { run(0); }, 600);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-date-picker-slide]').forEach(function (el) {
if (el._dps) return;
el._dps = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-date-picker-slide data-name="date" aria-label="Choose a date"></div>