Components / Form & Input / Calendar
Calendar stable
One month, standing on its own: a grid of real day buttons with today marked and a selection you can move.
<div class="k-cal" data-cal>
<div class="k-cal-h">
<button class="k-cal-nav" type="button" data-cal-prev aria-label="Previous month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m15 6-6 6 6 6"/></svg></button>
<span class="k-cal-title" data-cal-title>Month</span>
<button class="k-cal-nav" type="button" data-cal-next aria-label="Next month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
</div>
<div class="k-cal-wd" aria-hidden="true"><span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span></div>
<div class="k-cal-grid" data-cal-grid></div>
</div>/* KATA Calendar — one month of real buttons. */
.k-cal{font-family:var(--k-font);color:var(--k-ink);background:var(--k-bg);border:1px solid var(--k-line);border-radius:var(--k-radius-lg);padding:14px;width:288px}
.k-cal-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.k-cal-title{font-size:14px;font-weight:600;letter-spacing:-.01em}
.k-cal-nav{width:30px;height:30px;border-radius:8px;border:1px solid var(--k-line);background:var(--k-bg);color:var(--k-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer}
.k-cal-nav:hover{background:var(--k-bg-soft);color:var(--k-ink)}
.k-cal-wd{display:grid;grid-template-columns:repeat(7,1fr);margin-bottom:4px}
.k-cal-wd span{text-align:center;font-size:11px;font-weight:600;color:var(--k-ink-3);padding:4px 0}
.k-cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.k-cal-day{height:34px;border:0;border-radius:8px;background:none;font-family:inherit;font-size:13px;color:var(--k-ink);cursor:pointer;position:relative}
.k-cal-day:hover{background:var(--k-bg-soft)}
.k-cal-day.off{color:var(--k-ink-3);opacity:.55}
.k-cal-day.today{font-weight:650}
.k-cal-day.today::after{content:"";position:absolute;left:50%;bottom:4px;transform:translateX(-50%);width:5px;height:5px;border-radius:50%;background:var(--k-core)}
.k-cal-day.sel{background:var(--k-ink);color:var(--k-bg);font-weight:600}
.k-cal-day.sel::after{background:var(--k-bg)}
.k-cal.sm{width:248px;padding:11px}
.k-cal.sm .k-cal-day{height:29px;font-size:12.5px}
// KATA Calendar — renders one month of day buttons; the pick is yours.
// Set data-selected="YYYY-MM-DD" to start with a selection. Emits a
// "kata:date" CustomEvent (detail: "YYYY-MM-DD") on the element per pick.
document.querySelectorAll('[data-cal]').forEach(function (el) {
var title = el.querySelector('[data-cal-title]');
var grid = el.querySelector('[data-cal-grid]');
var sel = el.getAttribute('data-selected') || '';
var now = new Date();
var view = sel ? new Date(sel + 'T00:00:00') : new Date(now.getFullYear(), now.getMonth(), 1);
view = new Date(view.getFullYear(), view.getMonth(), 1);
var NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
var FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
var MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function iso(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
function render() {
title.textContent = NAMES[view.getMonth()] + ' ' + view.getFullYear();
grid.innerHTML = '';
var first = new Date(view.getFullYear(), view.getMonth(), 1);
var start = new Date(first); start.setDate(1 - first.getDay());
for (var i = 0; i < 42; i++) {
var d = new Date(start); d.setDate(start.getDate() + i);
var b = document.createElement('button');
b.type = 'button';
b.className = 'k-cal-day' + (d.getMonth() !== view.getMonth() ? ' off' : '') + (iso(d) === iso(now) ? ' today' : '') + (iso(d) === sel ? ' sel' : '');
b.textContent = d.getDate();
b.setAttribute('aria-label', FULL[d.getDay()] + ', ' + MON[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear());
if (iso(d) === iso(now)) b.setAttribute('aria-current', 'date');
b.setAttribute('data-date', iso(d));
grid.appendChild(b);
}
}
grid.addEventListener('click', function (e) {
var b = e.target.closest('.k-cal-day'); if (!b) return;
sel = b.getAttribute('data-date');
el.setAttribute('data-selected', sel);
el.dispatchEvent(new CustomEvent('kata:date', { detail: sel, bubbles: true }));
render();
});
el.querySelector('[data-cal-prev]').addEventListener('click', function () { view = new Date(view.getFullYear(), view.getMonth() - 1, 1); render(); });
el.querySelector('[data-cal-next]').addEventListener('click', function () { view = new Date(view.getFullYear(), view.getMonth() + 1, 1); render(); });
render();
});
Installation
CLI
node kata-ui.mjs add calendar
Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.
Manual
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.
Examples
With a selection
<div class="k-cal" data-cal data-selected="2026-11-14">
<div class="k-cal-h">
<button class="k-cal-nav" type="button" data-cal-prev aria-label="Previous month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m15 6-6 6 6 6"/></svg></button>
<span class="k-cal-title" data-cal-title>Month</span>
<button class="k-cal-nav" type="button" data-cal-next aria-label="Next month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
</div>
<div class="k-cal-wd" aria-hidden="true"><span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span></div>
<div class="k-cal-grid" data-cal-grid></div>
</div>Compact
<div class="k-cal sm" data-cal>
<div class="k-cal-h">
<button class="k-cal-nav" type="button" data-cal-prev aria-label="Previous month"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m15 6-6 6 6 6"/></svg></button>
<span class="k-cal-title" data-cal-title>Month</span>
<button class="k-cal-nav" type="button" data-cal-next aria-label="Next month"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
</div>
<div class="k-cal-wd" aria-hidden="true"><span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span></div>
<div class="k-cal-grid" data-cal-grid></div>
</div>Usage
The Date Picker’s other half, unboxed — for booking pages, availability and anywhere the month itself is the interface. It renders one month and reports the pick; ranges, min/max and disabled days are your layer on top of its day buttons.
Accessibility
Days are buttons with full date labels ("November 14, 2026"), today carries aria-current, and the month title is a live heading the arrows update. Arrow-key grid navigation is the app’s layer; the tab stop per day is native.
Anatomy
The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.
.k-cal | class |
.k-cal-h | class |
.k-cal-title | class |
.k-cal-nav | class |
.k-cal-wd | class |
.k-cal-grid | class |
.k-cal-day | class |
.off | class |
.today | class |
.sel | class |
.sm | class |
data-cal | data hook |
data-cal-prev | data hook |
data-cal-title | data hook |
data-cal-next | data hook |
data-cal-grid | data hook |
data-selected | data hook |
data-date | data hook |
aria-label | ARIA |
aria-hidden | ARIA |
Related parts
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Button Group stable
Buttons joined into one piece: shared edges, one radius, no gaps. For toolbars, paging and split actions.
Cascader beta
A select that walks a tree: pick a branch, then a branch of the branch, until the leaf is the answer.
Checkbox / Radio stable
Native inputs with a restyled face. Keyboard support and form behavior stay intact.