OTP Input
A one-time code field: one real input drawn as cells. Digits rise into place, paste and SMS autofill just work, and the cells turn to ink once complete.
View codeA real date input with a calendar: months slide the way you go, the month name rolls, and the ink mark travels to the day you pick. Full keyboard.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Use the Kodza Date Picker for the booking date in my form.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<label class="kodza-date" data-locale="en-GB">
<span class="kodza-date__label">Launch day</span>
<input class="kodza-date__native" type="date" name="launch" value="2026-10-23">
</label>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Date Picker
A real date input, dressed up: the field shows the date in words, and a calendar
unrolls under it. Months slide sideways in the direction you go, the month name rolls, and
the ink mark travels to the day you pick before the calendar closes. Today wears a ring;
days outside min / max are off. Arrows, Page Up / Down, Home / End, Enter, Escape.
Same box as Select. Everything is tuned with the --kz-date-* variables. */
.kodza-date {
--kz-date-bg: #ffffff;
--kz-date-fg: #0a0a0a;
--kz-date-muted: rgba(10, 10, 10, 0.6);
--kz-date-line: rgba(10, 10, 10, 0.14);
--kz-date-line-hover: rgba(10, 10, 10, 0.3);
--kz-date-soft: rgba(10, 10, 10, 0.06);
--kz-date-ink: #0a0a0a;
--kz-date-on-ink: #ffffff;
--kz-date-shadow: 0 18px 40px -16px rgba(10, 10, 10, 0.3);
--kz-date-height: 56px;
--kz-date-radius: 12px;
--kz-date-pad: 16px;
--kz-date-cell: 38px;
--kz-date-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
display: block;
width: 100%;
max-width: 320px;
/* The closed panel keeps its size, ready to unroll, but never lengthens the page */
contain: layout;
/* A number, not auto, so it can wait for the panel to roll up before dropping back */
z-index: 0;
transition: z-index 0s linear 0.4s;
color: var(--kz-date-fg);
}
/* Open, the field rises above what follows it (its panel included) */
.kodza-date.is-open {
z-index: 20;
transition-delay: 0s;
}
.kodza-date__label {
position: absolute;
top: 10px;
left: var(--kz-date-pad);
z-index: 1;
color: var(--kz-date-muted);
font-size: 12px;
line-height: 1;
pointer-events: none;
}
.kodza-date__native,
.kodza-date__trigger {
display: flex;
align-items: flex-end;
width: 100%;
height: var(--kz-date-height);
margin: 0;
padding: 0 44px 9px var(--kz-date-pad);
border: 0;
border-radius: var(--kz-date-radius);
background: var(--kz-date-bg);
box-shadow: inset 0 0 0 1px var(--kz-date-line);
box-sizing: border-box;
color: inherit;
font: inherit;
font-size: 15px;
line-height: 22px;
text-align: left;
cursor: pointer;
transition: box-shadow 0.25s ease;
}
.kodza-date__native:hover,
.kodza-date__trigger:hover {
box-shadow: inset 0 0 0 1px var(--kz-date-line-hover);
}
.kodza-date__native:focus-visible,
.kodza-date__trigger:focus-visible,
.kodza-date.is-open .kodza-date__trigger {
outline: none;
box-shadow: inset 0 0 0 1.5px var(--kz-date-fg), 0 0 0 4px var(--kz-date-soft);
}
.kodza-date.is-ready .kodza-date__native {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
clip-path: inset(50%);
}
.kodza-date__value {
display: inline-grid;
overflow: hidden;
}
.kodza-date__value > span {
grid-area: 1 / 1;
white-space: nowrap;
}
.kodza-date__value .is-empty {
color: var(--kz-date-muted);
}
.kodza-date__icon {
position: absolute;
top: calc(var(--kz-date-height) / 2 - 9px);
right: 16px;
width: 18px;
height: 18px;
fill: none;
stroke: var(--kz-date-muted);
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
/* The calendar unrolls under the field */
.kodza-date__pop {
position: absolute;
top: calc(var(--kz-date-height) + 8px);
left: 0;
z-index: 20;
width: calc(var(--kz-date-cell) * 7 + 24px);
padding: 12px;
border-radius: 16px;
background: var(--kz-date-bg);
box-shadow: 0 0 0 1px var(--kz-date-line), var(--kz-date-shadow);
box-sizing: border-box;
visibility: hidden;
opacity: 0;
clip-path: inset(0 0 100% 0 round 16px);
transform: translateY(-4px);
transition:
clip-path 0.4s var(--kz-date-ease),
opacity 0.2s ease,
transform 0.4s var(--kz-date-ease),
visibility 0s linear 0.4s;
}
.kodza-date.is-open .kodza-date__pop {
visibility: visible;
opacity: 1;
clip-path: inset(-40px -40px -60px -40px round 16px);
transform: none;
transition-delay: 0s;
}
.kodza-date.is-above .kodza-date__pop {
top: auto;
bottom: calc(var(--kz-date-height) + 8px);
clip-path: inset(100% 0 0 0 round 16px);
transform: translateY(4px);
}
.kodza-date.is-above.is-open .kodza-date__pop {
clip-path: inset(-60px -40px -40px -40px round 16px);
transform: none;
}
.kodza-date__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-left: 6px;
}
.kodza-date__month {
display: inline-grid;
overflow: hidden;
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
}
.kodza-date__month > span {
grid-area: 1 / 1;
white-space: nowrap;
}
.kodza-date__nav {
display: flex;
gap: 2px;
}
.kodza-date__nav button,
.kodza-date__today {
display: grid;
place-items: center;
height: 32px;
border: 0;
border-radius: 999px;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-date__nav button {
width: 32px;
padding: 0;
}
.kodza-date__nav button:hover,
.kodza-date__today:hover {
background: var(--kz-date-soft);
}
.kodza-date__nav svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.kodza-date__weekdays,
.kodza-date__days {
display: grid;
grid-template-columns: repeat(7, var(--kz-date-cell));
}
.kodza-date__weekdays {
margin: 0;
padding: 0;
list-style: none;
}
.kodza-date__weekdays span {
display: grid;
place-items: center;
height: 28px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11px;
color: var(--kz-date-muted);
}
/* Months share one cell: the old one leaves, the new one comes in from the side you go */
.kodza-date__stage {
display: grid;
overflow: hidden;
}
.kodza-date__days {
position: relative;
grid-area: 1 / 1;
grid-auto-rows: var(--kz-date-cell);
align-content: start;
height: calc(var(--kz-date-cell) * 6);
}
.kodza-date__day {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: var(--kz-date-cell);
height: var(--kz-date-cell);
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: inherit;
font: inherit;
font-size: 14px;
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: color 0.2s ease 0.1s, background-color 0.2s ease;
}
.kodza-date__day:hover {
background: var(--kz-date-soft);
}
.kodza-date__day.is-outside {
color: var(--kz-date-muted);
}
.kodza-date__day.is-today::after {
content: "";
position: absolute;
inset: 4px;
border-radius: 50%;
box-shadow: inset 0 0 0 1px var(--kz-date-line-hover);
}
.kodza-date__day[aria-selected="true"] {
color: var(--kz-date-on-ink);
}
.kodza-date__day[aria-selected="true"]:hover {
background: none;
}
.kodza-date__day:disabled {
color: var(--kz-date-muted);
opacity: 0.4;
cursor: not-allowed;
}
.kodza-date__day:focus-visible,
.kodza-date__nav button:focus-visible,
.kodza-date__today:focus-visible {
outline: 2px solid var(--kz-date-fg);
outline-offset: -2px;
}
/* The ink mark, one per month: it travels to the day you pick */
.kodza-date__mark {
position: absolute;
top: 0;
left: 0;
width: var(--kz-date-cell);
height: var(--kz-date-cell);
border-radius: 50%;
background: var(--kz-date-ink);
transform: translate(var(--kz-mark-x, 0), var(--kz-mark-y, 0));
transition: transform 0.4s var(--kz-date-ease), opacity 0.2s ease;
}
.kodza-date__mark.is-hidden {
opacity: 0;
transform: translate(var(--kz-mark-x, 0), var(--kz-mark-y, 0)) scale(0.4);
}
.kodza-date__foot {
display: flex;
justify-content: flex-end;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--kz-date-soft);
}
.kodza-date__today {
padding: 0 12px;
font-size: 13px;
font-weight: 500;
}
@media (prefers-reduced-motion: reduce) {
.kodza-date__native,
.kodza-date__trigger,
.kodza-date__pop,
.kodza-date__day,
.kodza-date__mark {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-date {
--kz-date-bg: #161616;
--kz-date-fg: #f2f2f0;
--kz-date-muted: rgba(242, 242, 240, 0.62);
--kz-date-line: rgba(255, 255, 255, 0.14);
--kz-date-line-hover: rgba(255, 255, 255, 0.3);
--kz-date-soft: rgba(255, 255, 255, 0.08);
--kz-date-ink: #f2f2f0;
--kz-date-on-ink: #0a0a0a;
--kz-date-shadow: 0 18px 40px -16px rgba(0, 0, 0, 0.8);
}
*/
/* kodza-guard 18d93fa7dbb5 — generated by Kodza, do not edit.
Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
component. :not(#_) is always true and weighs as much as an id, so these lines win over
theme rules made of classes and tags. Customise the component through its --kz-* variables. */
/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */
:where(.kodza-date__native):not(#_) {
outline: revert;
position: revert;
}
:where(.kodza-date__trigger):not(#_) {
outline: revert;
}
:where(.kodza-date__day):not(#_),
:where(.kodza-date__today):not(#_),
:where(.kodza-date__nav button:where(:not([class*="kodza-"]))):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-date:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-height: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-date__label:not(#_),
.kodza-date__value:not(#_),
.kodza-date__head:not(#_),
.kodza-date__month:not(#_),
.kodza-date__nav:not(#_),
.kodza-date__weekdays:not(#_),
.kodza-date__stage:not(#_),
.kodza-date__days:not(#_),
.kodza-date__mark:not(#_),
.kodza-date__foot:not(#_),
.kodza-date__value span:where(:not([class*="kodza-"])):not(#_),
.kodza-date__month span:where(:not([class*="kodza-"])):not(#_),
.kodza-date__weekdays span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-date__native:not(#_) {
text-decoration: revert;
list-style: revert;
float: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-date__trigger:not(#_) {
text-decoration: revert;
list-style: revert;
float: revert;
position: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-date__icon:not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-date__day:not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-date__today:not(#_),
.kodza-date__nav button:where(:not([class*="kodza-"])):not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-date__nav svg:where(:not([class*="kodza-"])):not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-date__icon:not(#_):where(:not([hidden])),
.kodza-date__nav svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-date__icon:not(#_):where([hidden]),
.kodza-date__nav svg:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
.kodza-date__day:not(#_)::after {
box-sizing: revert;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-date:not(#_) {
position: relative;
display: block;
transition: z-index 0s linear 0.4s;
color: var(--kz-date-fg);
}
.kodza-date:not(#_) {
width: min(100%, 320px);
}
.kodza-date.is-open:not(#_) {
transition-delay: 0s;
}
.kodza-date__native:not(#_),
.kodza-date__trigger:not(#_) {
display: flex;
align-items: flex-end;
width: 100%;
height: var(--kz-date-height);
margin: 0;
padding: 0 44px 9px var(--kz-date-pad);
border: 0;
border-radius: var(--kz-date-radius);
background: var(--kz-date-bg);
box-shadow: inset 0 0 0 1px var(--kz-date-line);
box-sizing: border-box;
color: inherit;
font: inherit;
font-size: 15px;
line-height: 22px;
text-align: left;
cursor: pointer;
transition: box-shadow 0.25s ease;
}
.kodza-date__native:hover:not(#_),
.kodza-date__trigger:hover:not(#_) {
box-shadow: inset 0 0 0 1px var(--kz-date-line-hover);
}
.kodza-date__native:focus-visible:not(#_),
.kodza-date__trigger:focus-visible:not(#_),
.kodza-date.is-open .kodza-date__trigger:not(#_) {
outline: none;
box-shadow: inset 0 0 0 1.5px var(--kz-date-fg), 0 0 0 4px var(--kz-date-soft);
}
.kodza-date.is-ready .kodza-date__native:not(#_) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
clip-path: inset(50%);
}
.kodza-date__icon:not(#_) {
position: absolute;
}
.kodza-date__nav button:not(#_),
.kodza-date__today:not(#_) {
display: grid;
place-items: center;
height: 32px;
border: 0;
border-radius: 999px;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-date__nav button:not(#_) {
width: 32px;
padding: 0;
}
.kodza-date__nav button:hover:not(#_),
.kodza-date__today:hover:not(#_) {
background: var(--kz-date-soft);
}
.kodza-date__day:not(#_) {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: var(--kz-date-cell);
height: var(--kz-date-cell);
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: inherit;
font: inherit;
font-size: 14px;
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: color 0.2s ease 0.1s, background-color 0.2s ease;
}
.kodza-date__day:hover:not(#_) {
background: var(--kz-date-soft);
}
.kodza-date__day.is-outside:not(#_) {
color: var(--kz-date-muted);
}
.kodza-date__day.is-today:not(#_)::after {
content: "";
position: absolute;
inset: 4px;
border-radius: 50%;
box-shadow: inset 0 0 0 1px var(--kz-date-line-hover);
}
.kodza-date__day[aria-selected="true"]:not(#_) {
color: var(--kz-date-on-ink);
}
.kodza-date__day[aria-selected="true"]:hover:not(#_) {
background: none;
}
.kodza-date__day:disabled:not(#_) {
color: var(--kz-date-muted);
opacity: 0.4;
cursor: not-allowed;
}
.kodza-date__day:focus-visible:not(#_),
.kodza-date__nav button:focus-visible:not(#_),
.kodza-date__today:focus-visible:not(#_) {
outline: 2px solid var(--kz-date-fg);
outline-offset: -2px;
}
.kodza-date__today:not(#_) {
padding: 0 12px;
font-size: 13px;
font-weight: 500;
}
@media (prefers-reduced-motion: reduce) {
.kodza-date__native:not(#_),
.kodza-date__trigger:not(#_),
.kodza-date__day:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Date Picker
// Builds a field and a calendar over the real <input type="date"> and keeps it in step (firing
// "input" and "change" on it). Weeks start on Monday (data-week-start="0" for Sunday), names
// come from data-locale (or the page language). The calendar is a grid of buttons with one
// focusable day: arrows move by day and week, Page Up / Down by month, Home / End to the
// start and end of the week, Enter picks, Escape closes and gives the focus back.
(() => {
const SELECTOR = '.kodza-date';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const parse = (value) => {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || '');
return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : null;
};
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
const same = (a, b) => a && b && iso(a) === iso(b);
const icon = '<svg class="kodza-date__icon" viewBox="0 0 18 18" aria-hidden="true"><rect x="2.5" y="3.5" width="13" height="12" rx="2.5"/><path d="M2.5 7.5h13M6 2v3M12 2v3"/></svg>';
const arrow = (d) => `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="${d}"/></svg>`;
let uid = 0;
// A word or a month name rolls to its new value (up going forward, down going back)
const roll = (box, text, dir = 1) => {
const prev = box.lastElementChild;
if (prev && prev.textContent === text) return;
const span = Object.assign(document.createElement('span'), { textContent: text });
box.append(span);
if (!prev) return;
if (reduceMotion) return prev.remove();
const timing = { duration: 420, easing: ease };
prev.animate([{ transform: 'none' }, { transform: `translateY(${dir * -110}%)` }], { ...timing, fill: 'forwards' }).finished.then(() => prev.remove());
span.animate([{ transform: `translateY(${dir * 110}%)` }, { transform: 'none' }], timing);
};
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const native = root.querySelector('.kodza-date__native');
const label = root.querySelector('.kodza-date__label');
if (!native) return;
const id = `kodza-date-${++uid}`;
const locale = root.dataset.locale || document.documentElement.lang || undefined;
const weekStart = root.dataset.weekStart === '0' ? 0 : 1;
const long = new Intl.DateTimeFormat(locale, { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });
const monthName = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' });
const dayName = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const weekday = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
const min = parse(native.min);
const max = parse(native.max);
const today = new Date();
// The field
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'kodza-date__trigger';
trigger.setAttribute('aria-haspopup', 'dialog');
trigger.setAttribute('aria-expanded', 'false');
if (label) {
label.id = `${id}-label`;
trigger.setAttribute('aria-labelledby', `${id}-label ${id}-value`);
}
const value = Object.assign(document.createElement('span'), { className: 'kodza-date__value', id: `${id}-value` });
trigger.append(value);
trigger.insertAdjacentHTML('beforeend', icon);
// The calendar
const pop = document.createElement('div');
pop.className = 'kodza-date__pop';
pop.setAttribute('role', 'dialog');
pop.setAttribute('aria-label', label ? `${label.textContent}: choose a date` : 'Choose a date');
pop.innerHTML = `
<div class="kodza-date__head">
<span class="kodza-date__month" aria-live="polite"></span>
<span class="kodza-date__nav">
<button type="button" data-step="-1" aria-label="Previous month">${arrow('M10 3.5 5.5 8l4.5 4.5')}</button>
<button type="button" data-step="1" aria-label="Next month">${arrow('M6 3.5 10.5 8 6 12.5')}</button>
</span>
</div>
<div class="kodza-date__weekdays" aria-hidden="true"></div>
<div class="kodza-date__stage"></div>
<div class="kodza-date__foot"><button class="kodza-date__today" type="button">Today</button></div>`;
native.after(trigger, pop);
native.tabIndex = -1;
native.setAttribute('aria-hidden', 'true');
const monthBox = pop.querySelector('.kodza-date__month');
const stage = pop.querySelector('.kodza-date__stage');
const weekdays = pop.querySelector('.kodza-date__weekdays');
for (let i = 0; i < 7; i++) {
const d = new Date(2024, 0, 1 + ((weekStart + i + 6) % 7));
weekdays.append(Object.assign(document.createElement('span'), { textContent: weekday.format(d) }));
}
let selected = parse(native.value);
let view = new Date((selected || today).getFullYear(), (selected || today).getMonth(), 1);
let focusDay = selected || today;
let grid = null;
const disabled = (d) => (min && d < min) || (max && d > max);
// One month: six weeks of buttons, and the ink mark on the chosen day
const build = (month) => {
const box = document.createElement('div');
box.className = 'kodza-date__days';
box.setAttribute('role', 'grid');
const mark = Object.assign(document.createElement('span'), { className: 'kodza-date__mark is-hidden' });
mark.setAttribute('aria-hidden', 'true');
box.append(mark);
const first = new Date(month.getFullYear(), month.getMonth(), 1);
const offset = (first.getDay() - weekStart + 7) % 7;
for (let i = 0; i < 42; i++) {
const d = addDays(first, i - offset);
const day = document.createElement('button');
day.type = 'button';
day.className = 'kodza-date__day';
day.textContent = d.getDate();
day.dataset.date = iso(d);
day.tabIndex = -1;
day.setAttribute('aria-label', dayName.format(d));
if (d.getMonth() !== month.getMonth()) day.classList.add('is-outside');
if (same(d, today)) {
day.classList.add('is-today');
day.setAttribute('aria-current', 'date');
}
day.disabled = !!disabled(d);
box.append(day);
}
return box;
};
const placeMark = (box, animate) => {
const mark = box.querySelector('.kodza-date__mark');
const day = selected && box.querySelector(`[data-date="${iso(selected)}"]:not(.is-outside)`);
box.querySelectorAll('.kodza-date__day').forEach((b) => b.setAttribute('aria-selected', String(b === day)));
if (!day) return mark.classList.add('is-hidden');
if (!animate) mark.style.transition = 'none';
mark.style.setProperty('--kz-mark-x', `${day.offsetLeft}px`);
mark.style.setProperty('--kz-mark-y', `${day.offsetTop}px`);
mark.classList.remove('is-hidden');
if (!animate) requestAnimationFrame(() => mark.style.removeProperty('transition'));
};
const setFocusable = () => {
grid.querySelectorAll('.kodza-date__day').forEach((b) => (b.tabIndex = b.dataset.date === iso(focusDay) ? 0 : -1));
};
// Show a month: the old grid leaves on one side, the new one comes in from the other
const render = (dir = 0) => {
const next = build(view);
const old = grid;
const hadFocus = old?.contains(document.activeElement);
grid = next;
stage.append(next);
roll(monthBox, monthName.format(view), dir || 1);
placeMark(next, false);
setFocusable();
// The grid that held the focus is going away: the focus moves to the new one
if (hadFocus) next.querySelector('[tabindex="0"]')?.focus({ preventScroll: true });
if (old) {
if (!dir || reduceMotion) old.remove();
else {
const timing = { duration: 380, easing: ease };
old.animate([{ transform: 'none', opacity: 1 }, { transform: `translateX(${dir * -40}%)`, opacity: 0 }], { ...timing, fill: 'forwards' }).finished.then(() => old.remove());
next.animate([{ transform: `translateX(${dir * 40}%)`, opacity: 0 }, { transform: 'none', opacity: 1 }], timing);
}
}
};
const showValue = (animate) => {
const text = selected ? long.format(selected) : 'Pick a date';
if (!animate) value.replaceChildren();
roll(value, text, 1);
value.lastElementChild.classList.toggle('is-empty', !selected);
};
const goToMonth = (d, dir) => {
view = new Date(d.getFullYear(), d.getMonth(), 1);
render(dir);
};
const moveFocus = (d) => {
if (d.getMonth() !== view.getMonth() || d.getFullYear() !== view.getFullYear()) goToMonth(d, d > focusDay ? 1 : -1);
focusDay = d;
setFocusable();
grid.querySelector(`[data-date="${iso(d)}"]`)?.focus({ preventScroll: true });
};
const isOpen = () => root.classList.contains('is-open');
const open = () => {
if (isOpen()) return;
focusDay = selected || today;
if (!grid || view.getMonth() !== focusDay.getMonth() || view.getFullYear() !== focusDay.getFullYear()) {
view = new Date(focusDay.getFullYear(), focusDay.getMonth(), 1);
grid?.remove();
grid = null;
render(0);
}
setFocusable();
// Not enough room below: the calendar opens upwards. A closed panel that changes side gets
// there at once, so it always unrolls from the field
const box = trigger.getBoundingClientRect();
const above = box.bottom + 380 > innerHeight && box.top > 380;
if (above !== root.classList.contains('is-above')) {
pop.style.transition = 'none';
root.classList.toggle('is-above', above);
void pop.offsetWidth;
pop.style.transition = '';
}
root.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
requestAnimationFrame(() => grid.querySelector('[tabindex="0"]')?.focus({ preventScroll: true }));
};
const close = (refocus = true) => {
if (!isOpen()) return;
root.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
if (refocus) trigger.focus({ preventScroll: true });
};
const pick = (d) => {
if (!d || disabled(d)) return;
const changed = !same(d, selected);
if (d.getMonth() !== view.getMonth() || d.getFullYear() !== view.getFullYear()) goToMonth(d, d > (selected || view) ? 1 : -1);
selected = d;
focusDay = d;
placeMark(grid, true);
setFocusable();
native.value = iso(d);
showValue(true);
if (changed) {
native.dispatchEvent(new Event('input', { bubbles: true }));
native.dispatchEvent(new Event('change', { bubbles: true }));
}
// Let the mark land, then close
setTimeout(() => !signal.aborted && close(), reduceMotion ? 0 : 320);
};
trigger.addEventListener('click', () => (isOpen() ? close() : open()), { signal });
pop.querySelectorAll('[data-step]').forEach((button) => button.addEventListener('click', () => {
const step = Number(button.dataset.step);
goToMonth(new Date(view.getFullYear(), view.getMonth() + step, 1), step);
focusDay = new Date(view.getFullYear(), view.getMonth(), Math.min(focusDay.getDate(), 28));
setFocusable();
}, { signal }));
pop.querySelector('.kodza-date__today').addEventListener('click', () => pick(new Date(today.getFullYear(), today.getMonth(), today.getDate())), { signal });
stage.addEventListener('click', (event) => {
const day = event.target.closest('.kodza-date__day');
if (day && !day.disabled) pick(parse(day.dataset.date));
}, { signal });
pop.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
return close();
}
if (!event.target.classList.contains('kodza-date__day')) return;
const dow = (focusDay.getDay() - weekStart + 7) % 7;
// The same day a month away, or that month's last one (31 March, then 30 April)
const monthAway = (n) => {
const last = new Date(focusDay.getFullYear(), focusDay.getMonth() + n + 1, 0).getDate();
return new Date(focusDay.getFullYear(), focusDay.getMonth() + n, Math.min(focusDay.getDate(), last));
};
const moves = {
ArrowLeft: () => addDays(focusDay, -1),
ArrowRight: () => addDays(focusDay, 1),
ArrowUp: () => addDays(focusDay, -7),
ArrowDown: () => addDays(focusDay, 7),
Home: () => addDays(focusDay, -dow),
End: () => addDays(focusDay, 6 - dow),
PageUp: () => monthAway(-1),
PageDown: () => monthAway(1),
};
if (moves[event.key]) {
event.preventDefault();
moveFocus(moves[event.key]());
} else if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
pick(focusDay);
}
}, { signal });
// A click on the label text opens the calendar instead of reaching the hidden input
root.addEventListener('click', (event) => {
if (trigger.contains(event.target) || pop.contains(event.target)) return;
event.preventDefault();
trigger.focus();
}, { signal });
pop.addEventListener('click', (event) => event.preventDefault(), { signal });
document.addEventListener('pointerdown', (event) => !root.contains(event.target) && close(false), { signal });
// Escape closes it even if the focus wandered off to the page
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && isOpen() && !root.contains(document.activeElement)) close();
}, { signal });
root.addEventListener('focusout', (event) => {
if (isOpen() && event.relatedTarget && !root.contains(event.relatedTarget)) close(false);
}, { signal });
// The calendar is built right away, so the panel has its real size before it first opens
render(0);
showValue(false);
}
// Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
// event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
// markup back. For themes and apps that swap content without a page load.
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const init = (scope) => find(scope).forEach((root) => {
if (root.classList.contains('is-ready')) return; // already running, maybe from another copy of this script
const controller = new AbortController();
instances.set(root, { controller, html: root.innerHTML, attrs: [...root.attributes].map((a) => [a.name, a.value]) });
mount(root, controller.signal);
root.classList.add('is-ready');
});
const destroy = (scope) => find(scope).forEach((root) => {
const instance = instances.get(root);
if (!instance) return;
instances.delete(root);
instance.controller.abort();
[...root.attributes].forEach((a) => root.removeAttribute(a.name));
instance.attrs.forEach(([name, value]) => root.setAttribute(name, value));
root.innerHTML = instance.html;
});
document.addEventListener('kodza:init', (event) => init(event.detail?.root));
document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
init();
// Loaded in <head>, the script runs before the markup exists: once more when it is parsed
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
'use client';
// Kodza — Date Picker, for React. From https://kodza.dev/components/date-picker/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: date-picker.css and date-picker.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './date-picker.css';
export type DatePickerProps = Omit<ComponentPropsWithoutRef<'label'>, "children" | "vars" | "locale"> & {
/** data-locale on the root (default: en-GB) */
locale?: "en-GB";
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-date-bg"?: string | number;
/** default: #0a0a0a */
"--kz-date-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.6) */
"--kz-date-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.14) */
"--kz-date-line"?: string | number;
/** default: rgba(10, 10, 10, 0.3) */
"--kz-date-line-hover"?: string | number;
/** default: rgba(10, 10, 10, 0.06) */
"--kz-date-soft"?: string | number;
/** default: #0a0a0a */
"--kz-date-ink"?: string | number;
/** default: #ffffff */
"--kz-date-on-ink"?: string | number;
/** default: 0 18px 40px -16px rgba(10, 10, 10, 0.3) */
"--kz-date-shadow"?: string | number;
/** default: 56px */
"--kz-date-height"?: string | number;
/** default: 12px */
"--kz-date-radius"?: string | number;
/** default: 16px */
"--kz-date-pad"?: string | number;
/** default: 38px */
"--kz-date-cell"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-date-ease"?: string | number;
};
/** The text, plain: the script reads it and rebuilds the markup around it */
children?: string;
className?: string;
};
export function DatePicker({ locale = "en-GB", vars, children = "Launch day", className, style, ...rest }: DatePickerProps) {
const ref = useRef<ComponentRef<'label'>>(null);
const classes = ["kodza-date", className].filter(Boolean).join(' ');
// Everything the markup is made of: when it changes, React gets a fresh element (see the top)
const key = JSON.stringify([classes, locale, vars, children, style, rest]);
// Starts the script on this element once React has rendered it, stops it when it goes
useEffect(() => {
const root = ref.current;
let live = true;
// @ts-ignore: a plain script, no types to find
import('./date-picker.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
return (
<label key={key} {...rest} className={classes} data-locale={locale} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<span className="kodza-date__label">{children}</span>
{' '}
<input className="kodza-date__native" type="date" name="launch" defaultValue="2026-10-23" />
</label>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.