Headless as-you-type behavior for a single <input> date field. Day-month-year,
zero dependencies, no calendar, no React in the core.
|
incomplete
Type 4, then 9, then 2026. Backspace through the separators.
npm install ictus
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.
Same machine, different separators, and the parse/format seam.
Default separator. Day overflow-pads 4–9, month 2–9.
|
incomplete
Typing ., /, or - still writes /.
|
incomplete
Still day-month-year. Year never pads on the first digit.
|
incomplete
Local calendar parts, padded, with the separator you pass.
formatDate =
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| |
Four exports. Callers own the DOM; apply owns the next value and caret.
applyTake 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 }
valuecaretselectionEnd?caret.key. / -, Backspace, or
Delete — the KeyboardEvent.key values.
separator?".". 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
formatDateWrites 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();
type ApplyInput = {
value: string;
caret: number;
selectionEnd?: number;
key: string;
separator?: string;
};
type ApplyResult = {
value: string;
caret: number;
};
<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} />;
}