ictus

v0.1.2

Headless as-you-type behavior for a single <input> date field. Day-month-year, zero dependencies, no calendar, no React in the core.

Demos · Size · API



| incomplete

Type 4, then 9, then 2026. Backspace through the separators.

npm install ictus

Size and speed

Min+gzip, as a bundler ships it. Reproduce with pnpm measure.

Entry minify gzip
ictus 2.4 kB 1.1 kB
ictus/react (react external) 0.7 kB 0.4 kB

apply is 0.1–0.2 µs per keystroke (about 5–8 million ops/s). Typing 11.12.2026 is about 2 µs. parseDate is about 0.4 µs. A 16 ms frame is tens of thousands of keystrokes. The machine walks at most ten characters and does not touch the DOM.

Demos

Same machine, different separators, and the parse/format seam.

Dot

Default separator. Day overflow-pads 4–9, month 2–9.



| incomplete

Slash

Typing ., /, or - still writes /.



| incomplete

Dash

Still day-month-year. Year never pads on the first digit.



| incomplete

formatDate

Local calendar parts, padded, with the separator you pass.

formatDate =

Acceptance table

Click a row to run that keystroke. | is the caret.



| incomplete

Before Key After
| 4 04.|
| 1 1|
1| . 01.|
13| . 13.|
| . |
04.| . 04.|
04.| 9 04.09.|
3| 9 3|
04.1| 3 04.1|
04.|2.2026 1 04.12.|2026
04.|1.2026 2 04.|1.2026
11| 1 11.1|
11.12.| ⌫ 11.12|

API

Four exports. Callers own the DOM; apply owns the next value and caret.

apply

Take the previous masked value, the selection, and the key just pressed. Returns the next value and caret. Unknown keys are a no-op. The box is never cleared on reject. A non-empty selection is replaced: Backspace and Delete remove it, a digit types into the hole.

import { apply } from "ictus";

apply({
  value: "1",
  caret: 1,
  key: ".",
  separator: ".", // default
});
// { value: "01.", caret: 3 }
value
Current masked string.
caret
Selection start (or the collapsed caret).
selectionEnd?
Selection end. Defaults to caret.
key
A digit, . / -, Backspace, or Delete — the KeyboardEvent.key values.
separator?
Written separator. Default ".". Use a single character.

parseDate

Returns a local Date (new Date(year, monthIndex, day)) only for a complete, calendar-valid triple. Partial strings and impossible dates return undefined. Separators ., /, and - are all accepted.

parseDate("11.12.2026"); // local Date
parseDate("11/12/2026"); // same
parseDate("32.01.2020"); // undefined
parseDate("31.02.2020"); // undefined
parseDate("11.12");      // undefined




    

formatDate

Writes dd{sep}mm{sep}yyyy from the date’s local day, month, and year.

formatDate(new Date(2026, 11, 11));
// "11.12.2026"

formatDate(new Date(2026, 11, 11), "/");
// "11/12/2026"

isDateMaskKey

True for keys apply handles. Use it to preventDefault in keydown without swallowing arrows, tab, or shortcuts.

if (!isDateMaskKey(event.key)) return;
event.preventDefault();

Types

type ApplyInput = {
  value: string;
  caret: number;
  selectionEnd?: number;
  key: string;
  separator?: string;
};

type ApplyResult = {
  value: string;
  caret: number;
};

Vanilla <input>

import { apply, isDateMaskKey } from "ictus";

const input = document.querySelector("input");
input.addEventListener("keydown", (event) => {
  if (!isDateMaskKey(event.key)) return;
  event.preventDefault();
  const next = apply({
    value: input.value,
    caret: input.selectionStart ?? 0,
    selectionEnd: input.selectionEnd ?? undefined,
    key: event.key,
  });
  input.value = next.value;
  input.setSelectionRange(next.caret, next.caret);
});

useDateFieldMask

Optional React entry. Peer on react. Restores the caret after commit. parsed is a local Date or undefined.

import { useDateFieldMask } from "ictus/react";

function DateInput() {
  const { inputProps, parsed } = useDateFieldMask();
  return <input {...inputProps} />;
}