timepicker-ui
4.7.0GitHub
timepicker-ui
Features / Validation

Validation

Built-in validation with custom rules and error handling

Built-in Validation

#

The picker checks the value already in the input, the values people pick, and its own options:

  • When the picker opens, it reads the input value. If it cannot read it, it shows labels.invalidTimeFormat after the input and emits error. An empty input is not an error.
  • It reads 2:30 PM or 02:30 pm on a 12-hour clock (the default) and 14:30 on a 24-hour clock. With clock.showSeconds, seconds are optional: 14:30:15.
  • Times blocked by clock.disabledTime are shown as disabled on the clock face and the wheel (or left out, with wheel.hideDisabled) and cannot be picked there. clock.isTimeDisabled works the same way. A time is disabled when clock.disabledTime or the function disables it. See Disabled Time.
  • The hour and minute fields inside the picker keep typed values in range: hours 1-12 or 0-23, minutes 0-59.
  • create() and setOptions() check clock.disabledTime and report invalid values through error.

Valid Time Detection

#

Read the selected time in callbacks.onConfirm or with picker.on('confirm'). Both run after the picker has written the new value to the input:

index.ts
import { TimepickerUI } from 'timepicker-ui';
const picker = new TimepickerUI('#timepicker', {
callbacks: {
onConfirm: (data) => {
console.log(data.hour, data.minutes, data.type); // 02 30 PM
}
}
});
picker.create();
picker.on('confirm', () => {
console.log(picker.getValue().time); // 02:30 PM
});

The payload has hour and minutes, type ('AM' or 'PM', undefined on a 24-hour clock) and, with clock.showSeconds, seconds. The time values are strings. With wheel.commitOnScroll, confirm also fires when a wheel stops scrolling, with autoCommit: true.

The picker sets input.value directly and does not fire input or change events. A listener on the input only sees what people type into it, so check picked values in onConfirm.

Error Handling

#

The error event and callbacks.onError report four kinds of problems: an input value the picker cannot read, a setValue() string that is not a time, invalid options, and a mode or feature whose plugin is not registered (for example ui.mode: 'wheel' without WheelPlugin).

Invalid Input Detection

Each time the picker opens, it reads the input value. When it cannot, for example 25:00 on a 24-hour clock or 2:30 without AM/PM on a 12-hour clock, it adds the tp-ui-invalid-format class to the input, puts a message right after the input and emits error:

index.ts
const picker = new TimepickerUI('#timepicker', {
clock: { type: '24h' },
labels: { invalidTimeFormat: 'Enter a time as HH:MM' },
callbacks: {
onError: (data) => {
console.warn(data.error, data.inputHour, data.inputMinute);
}
}
});
picker.create();
// Input value '25:00', picker opened:
// Invalid 24h time. 25 0

The message the picker inserts after the input:

index.html
<div class="tp-ui-invalid-text" role="alert"><b>Enter a time as HH:MM</b></div>

role="alert" makes screen readers announce it. The text comes from labels.invalidTimeFormat (default Invalid time format). The picker still opens. The message and the class are removed when a time is confirmed or set with setValue(), and on the next open if the value can be read by then. With range.enabled this check is skipped.

setValue() converts a time in the other clock format: 14:30 sets 02:30 PM on a 12-hour clock. A string that is not a time, such as 25:00 or abc, leaves the value and the input as they were and emits error. It does not add the class or the message.

Disabled Time Validation

create() and setOptions() check clock.disabledTime. An hour, minute or second out of range, or a time range that cannot be parsed or overlaps another, is reported through error:

index.ts
const picker = new TimepickerUI('#timepicker', {
clock: {
disabledTime: { hours: [0, 22, 23] }
},
callbacks: {
onError: (data) => console.error(data.error)
}
});
picker.create();
// [TimepickerUI] Invalid hours or minutes in disabledTime option
console.log(picker.isDestroyed); // true

The default clock is 12-hour, where hours takes 1 to 12, so 0, 22 and 23 are rejected. Set clock.type: '24h' to use them. After a failed create() the picker is not set up and does not open. On a working picker, a failed setOptions() keeps the previous options:

index.ts
const picker = new TimepickerUI('#meeting-time', {
callbacks: {
onError: (data) => console.error(data.error)
}
});
picker.create();
picker.setOptions({
clock: {
disabledTime: {
ranges: ['9:00 AM - 11:00 AM', '10:00 AM - 12:00 PM']
}
}
});
// Logs that the two ranges overlap; the previous options stay

If you use picker.on('error', ...) instead of callbacks.onError, subscribe before create(), so the handler is in place when the options are checked.

Error Payload

  • error: a short English description, for example Invalid 24h time. Use it for logging. The text people see comes from labels.invalidTimeFormat.
  • inputHour, inputMinute: the hour and minute read from the input.
  • inputSecond: the seconds, only with clock.showSeconds.
  • inputType: the AM/PM part as typed.
  • inputLength: the length of the value, set when the AM/PM part or the length is wrong.

Which input* fields are set depends on what went wrong. Option, plugin and setValue() errors carry only error. rejectedHour and rejectedMinute are deprecated and always undefined.

Custom Validation Rules

#

clock.disabledTime covers fixed rules. For anything else, such as a limit that changes or a rule that depends on another field, check the value in callbacks.onConfirm. The input already holds the new value at that point, so you can clear it and show your own message. The picker closes either way.

Minimum Time Validation

index.html
<label for="pickup-time">Pickup time</label>
<input id="pickup-time" type="text" aria-describedby="pickup-time-error" />
<p id="pickup-time-error" role="alert"></p>
index.ts
import { TimepickerUI } from 'timepicker-ui';
const input = document.querySelector<HTMLInputElement>('#pickup-time')!;
const message = document.querySelector<HTMLElement>('#pickup-time-error')!;
function toMinutes(hour = '0', minutes = '0', type?: string): number {
let h = Number(hour);
if (type === 'PM' && h < 12) h += 12;
if (type === 'AM' && h === 12) h = 0;
return h * 60 + Number(minutes);
}
function showError(text: string): void {
message.textContent = text;
input.setAttribute('aria-invalid', 'true');
}
function clearError(): void {
message.textContent = '';
input.removeAttribute('aria-invalid');
}
const earliest = toMinutes('10', '30', 'AM');
const picker = new TimepickerUI(input, {
callbacks: {
onConfirm: ({ hour, minutes, type }) => {
if (toMinutes(hour, minutes, type) < earliest) {
input.value = '';
showError('Pickup starts at 10:30 AM.');
return;
}
clearError();
}
}
});
picker.create();

toMinutes works for both clock types; type is undefined on a 24-hour clock. The message element has role="alert", so screen readers announce it when its text changes.

Time Range Validation

For a fixed window, use clock.disabledTime.ranges instead of a check after the fact. Times outside the window cannot be picked on the clock face or the wheel:

index.ts
const picker = new TimepickerUI('#meeting-time', {
clock: {
type: '24h',
disabledTime: {
ranges: ['00:00 - 08:59', '17:01 - 23:59']
}
}
});
picker.create();

Ranges include both ends, so this leaves 09:00 to 17:00. If people can type into the hour and minute fields (ui.editable or the keyboard view), also check the confirmed value as in the example above.

Date-Time Validation

index.ts
const dateInput = document.querySelector<HTMLInputElement>('#pickup-date')!;
const picker = new TimepickerUI(input, {
clock: { type: '24h' },
callbacks: {
onConfirm: ({ hour, minutes }) => {
if (!dateInput.value) {
showError('Pick a date first.');
return;
}
const [year, month, day] = dateInput.value.split('-').map(Number);
const selected = new Date(year, month - 1, day, Number(hour), Number(minutes));
if (selected.getTime() <= Date.now()) {
input.value = '';
showError('Pick a time later than now.');
return;
}
clearError();
}
}
});
picker.create();

#pickup-date is an <input type="date">, whose value is YYYY-MM-DD. input, showError and clearError come from the first example.

HTML5 Validation

#

Native form validation works on the input the picker is attached to. Use type="text". A type="time" input only takes values like 14:30, so it drops 12-hour values such as 02:30 PM, and the browser adds its own time controls. min, max and step apply only to type="time". Use clock.disabledTime and clock.incrementMinutes instead.

Required Time Input

index.html
<form>
<label for="meeting-time">Meeting time</label>
<input id="meeting-time" type="text" required />
<button type="submit">Schedule</button>
</form>
index.ts
const picker = new TimepickerUI('#meeting-time', {
clock: { type: '24h' }
});
picker.create();

With required, the browser blocks the submit and shows its own message while the input is empty. No extra code is needed.

Custom Validation Messages

setCustomValidity() adds your own rule to the browser's check. Call it from onConfirm for picked values and from change for typed ones:

index.ts
const input = document.querySelector<HTMLInputElement>('#meeting-time')!;
function checkBusinessHours(): void {
const { hour, minutes } = picker.getValue();
const total = Number(hour) * 60 + Number(minutes);
const outside = total < 9 * 60 || total > 17 * 60;
input.setCustomValidity(outside ? 'Business hours are 09:00 to 17:00.' : '');
}
const picker = new TimepickerUI(input, {
clock: { type: '24h' },
callbacks: { onConfirm: checkBusinessHours }
});
picker.create();
input.addEventListener('change', checkBusinessHours);

During onConfirm the input already holds the confirmed time, so getValue() returns it. The browser shows the message when the form is submitted.

Visual Error States

#

When the picker cannot read the input value, it adds tp-ui-invalid-format to the input and inserts a tp-ui-invalid-text element after it. main.css gives the input a 2px border and colors its text and the message with --tp-error. The message text comes from labels.invalidTimeFormat.

Error Styling

index.css
:root {
--tp-error: #b3261e;
}
.tp-ui-invalid-format {
border-radius: 8px;
}
.tp-ui-invalid-text {
margin-top: 4px;
font-size: 0.875rem;
}

The message is text, so the error is not shown by color alone.

Complete Example

The picker's own check for values it cannot read, plus a rule of your own with its own message. Both states get the same border:

index.html
<div class="field">
<label for="pickup-time">Pickup time</label>
<input id="pickup-time" type="text" aria-describedby="pickup-time-error" />
<p id="pickup-time-error" class="field-error" role="alert"></p>
</div>
<style>
.field input {
border: 2px solid #d1d5db;
border-radius: 8px;
padding: 8px 12px;
font-size: 16px;
}
.field input[aria-invalid='true'],
.field input.tp-ui-invalid-format {
border-color: #b3261e;
}
.field-error {
margin-top: 4px;
color: #b3261e;
font-size: 0.875rem;
}
</style>
index.ts
import { TimepickerUI } from 'timepicker-ui';
import 'timepicker-ui/main.css';
const input = document.querySelector<HTMLInputElement>('#pickup-time')!;
const message = document.querySelector<HTMLElement>('#pickup-time-error')!;
function setError(text: string): void {
message.textContent = text;
if (text) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
}
const picker = new TimepickerUI(input, {
clock: { type: '24h' },
labels: { invalidTimeFormat: 'Enter a time as HH:MM' },
callbacks: {
onConfirm: ({ hour, minutes }) => {
const total = Number(hour) * 60 + Number(minutes);
if (total < 10 * 60 + 30) {
input.value = '';
setError('Pickup starts at 10:30.');
return;
}
setError('');
},
onError: (data) => console.warn(data.error)
}
});
picker.create();
Server-Side Validation
Check the time again on your server. Anything that runs in the browser, including clock.disabledTime and the checks on this page, can be bypassed.
Learn about events
Esc