Components / Form & Input / Time Picker

Time Picker stable

The Date Picker’s other half: an input with two scrolling columns of hours and minutes.

Preview

Open it, pick an hour and a minute — or just type. The panel closes on Escape or a click away.

Uses: Input — include its CSS alongside this one.

<div class="k-tp" data-tp style="max-width:220px">
  <div class="wrap-in">
    <input class="input" data-tp-input type="text" value="14:30" placeholder="HH:MM" aria-label="Time" style="width:100%;padding-right:42px">
    <button class="k-tp-btn" data-tp-btn type="button" aria-label="Open time picker" aria-expanded="false"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></svg></button>
  </div>
  <div class="k-tp-panel" data-tp-panel role="dialog" aria-label="Choose a time" hidden>
    <div class="k-tp-col" data-tp-h><span class="hd">HOUR</span></div>
    <div class="k-tp-col" data-tp-m><span class="hd">MIN</span></div>
  </div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Open it, pick an hour and a minute — or just type. The panel closes on Escape or a click away.</p>
/* KATA Time Picker — two columns, one value. */
.k-tp{position:relative;font-family:var(--k-font);color:var(--k-ink)}
.k-tp .wrap-in{position:relative}
.k-tp-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:28px;height:28px;border:0;border-radius:7px;background:none;color:var(--k-ink-3);display:flex;align-items:center;justify-content:center;cursor:pointer}
.k-tp-btn:hover{background:var(--k-bg-soft);color:var(--k-ink)}
.k-tp-panel{position:absolute;inset-inline-start:0;top:calc(100% + 8px);z-index:50;background:var(--k-bg);border:1px solid var(--k-line);border-radius:14px;box-shadow:0 20px 44px -14px rgba(16,24,40,.22);padding:10px;display:flex;gap:6px}
.k-tp-panel[hidden]{display:none}
.k-tp-col{display:flex;flex-direction:column;gap:2px;max-height:196px;overflow:auto;width:64px;padding-inline-end:2px}
.k-tp-col .hd{font-size:10.5px;font-weight:600;letter-spacing:.08em;color:var(--k-ink-3);text-align:center;padding:3px 0 5px;position:sticky;top:0;background:var(--k-bg)}
.k-tp-opt{padding:7px 0;border:0;border-radius:8px;background:none;font-family:inherit;font-size:13.5px;color:var(--k-ink-2);cursor:pointer;text-align:center}
.k-tp-opt:hover{background:var(--k-bg-soft);color:var(--k-ink)}
.k-tp-opt.sel{background:var(--k-ink);color:var(--k-bg);font-weight:600}
// KATA Time Picker — hours and five-minute steps; typing still works.
document.querySelectorAll('[data-tp]').forEach(function (el) {
  var input = el.querySelector('[data-tp-input]');
  var btn = el.querySelector('[data-tp-btn]');
  var panel = el.querySelector('[data-tp-panel]');
  var hCol = el.querySelector('[data-tp-h]');
  var mCol = el.querySelector('[data-tp-m]');
  function pad(n) { return String(n).padStart(2, '0'); }
  function build(col, values, cur) {
    col.querySelectorAll('.k-tp-opt').forEach(function (o) { o.remove(); });
    values.forEach(function (v) {
      var b = document.createElement('button');
      b.type = 'button'; b.className = 'k-tp-opt' + (v === cur ? ' sel' : ''); b.textContent = v; b.setAttribute('data-v', v);
      col.appendChild(b);
    });
  }
  function render() {
    var m = /^(\d{1,2}):(\d{2})/.exec(input.value || '');
    build(hCol, Array.from({ length: 24 }, function (_, i) { return pad(i); }), m ? pad(+m[1]) : '');
    build(mCol, Array.from({ length: 12 }, function (_, i) { return pad(i * 5); }), m ? m[2] : '');
  }
  function open() { render(); panel.hidden = false; btn.setAttribute('aria-expanded', 'true'); }
  function close() { panel.hidden = true; btn.setAttribute('aria-expanded', 'false'); }
  btn.addEventListener('click', function () { panel.hidden ? open() : close(); });
  input.addEventListener('focus', open);
  panel.addEventListener('click', function (e) {
    var b = e.target.closest('.k-tp-opt'); if (!b) return;
    var m = /^(\d{1,2}):(\d{2})/.exec(input.value || '');
    var h = m ? pad(+m[1]) : '09', min = m ? m[2] : '00';
    if (b.parentElement === hCol) h = b.getAttribute('data-v'); else min = b.getAttribute('data-v');
    input.value = h + ':' + min;
    input.dispatchEvent(new Event('change', { bubbles: true }));
    render();
  });
  document.addEventListener('click', function (e) { if (!el.contains(e.target)) close(); });
  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
  render();
});

Installation

CLI

node kata-ui.mjs add time-picker

Writes the part’s CSS and JS into your project. Dependencies are resolved and written with it. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

Minutes step by five, the way appointments actually work; type the value directly when you know it — the panel is a courtesy, not a gate. The pick writes HH:MM (24-hour) into the input.

Accessibility

The columns are lists of buttons with plain labels ("14", "05") under hour and minute headings; the input stays editable by keyboard, Escape closes the panel, and the toggle button is named for what it opens.

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-tpclass
.wrap-inclass
.k-tp-btnclass
.k-tp-panelclass
.k-tp-colclass
.hdclass
.k-tp-optclass
.selclass
data-tpdata hook
data-tp-inputdata hook
data-tp-btndata hook
data-tp-paneldata hook
data-tp-hdata hook
data-tp-mdata hook
data-vdata hook
aria-labelARIA
aria-expandedARIA
aria-hiddenARIA

Related parts