A time picker for web forms. Attach it to an <input> and users pick a time on a clock face or a scroll wheel.
Live demo · Documentation · Changelog · React wrapper
window or document on import, so it’s safe in Next.js, Nuxt and other SSR setups.labels.HH:MM:SS) in the clock, wheel and compact wheelclock.isTimeDisabled)npm install timepicker-ui
<input id="timepicker" type="text" />
import { TimepickerUI } from "timepicker-ui";
import "timepicker-ui/main.css";
const input = document.querySelector("#timepicker");
const picker = new TimepickerUI(input, {
clock: { type: "24h" },
ui: { theme: "dark" },
callbacks: {
onConfirm: (data) => console.log("Selected:", data),
},
});
picker.create();
No global CSS reset is needed: the picker sets box-sizing: border-box on its own elements (details).
picker.create(); // render the picker and attach it to the input
picker.open();
picker.close();
picker.destroy();
picker.getValue(); // { hour, minutes, seconds, type, time }, seconds only with clock.showSeconds
picker.setValue("14:30"); // "2:30 PM" works too
picker.setValue("14:30:15"); // with clock.showSeconds
picker.setOptions({ ui: { theme: "crane" } });
picker.setRange({ from: "09:00", to: "17:00" }); // needs RangePlugin
picker.getRange();
picker.setActivePart("to");
picker.refreshDisabledTime(); // after the data behind clock.isTimeDisabled changes
const off = picker.on("confirm", (data) => {}); // on() and once() return a function that removes the handler
off();
picker.on("confirm", (data) => {});
picker.on("cancel", (data) => {});
picker.on("open", () => {});
picker.on("update", (data) => {});
picker.on("clear", (data) => {});
picker.on("error", (data) => {});
picker.once("confirm", handler);
picker.off("confirm", handler);
TimepickerUI.getById("my-id");
TimepickerUI.getAllInstances();
TimepickerUI.isAvailable("#timepicker");
TimepickerUI.destroyAll();
Grouped by area. Pass only what you need, the rest has defaults (full list).
new TimepickerUI(input, {
clock: {
type: "12h" | "24h" | "auto", // default "12h"; "auto" follows the browser locale
incrementHours: 1,
incrementMinutes: 1,
showSeconds: false, // true adds seconds (HH:MM:SS)
incrementSeconds: 1,
disabledTime: { hours: [], minutes: [], seconds: [], ranges: "" },
isTimeDisabled: ({ hour, minutes, seconds }) => false, // hour is 0-23
defaultTime: "now" | { time, updateInput },
},
ui: {
theme: "basic" | "dark" | "m3-green" | "crane" | "blueprint" | ...,
mode: "clock" | "wheel" | "compact-wheel", // default "clock"
animation: true,
backdrop: true,
initialView: "clock", // or "keyboard"
editable: false,
inline: { enabled: false, container: "#picker" | element, showButtons: false, autoUpdate: false, embedded: false },
clearButton: false, // or { clearInput: false } to keep the input value on Clear
cssClass: "",
},
labels: { am, pm, ok, cancel, clear, clockTitle, hour, minute, second, rangeFrom, rangeTo, timezone, ... },
behavior: { focusTrap: true, returnFocus: false, delayHandler: 0, id: "" },
callbacks: { onConfirm, onCancel, onOpen, onUpdate, onSelectHour, onSelectMinute, onSelectSecond, onSelectPeriod, onClear, onError, ... },
wheel: { placement: "auto" | "top" | "bottom", hideFooter: false, commitOnScroll: false, hideDisabled: false, ignoreOutsideClick: false },
range: { enabled: false, linked: true, minDuration, maxDuration },
timezone: { enabled: false, default, list },
});
Preview all of them: basic, crane, crane-straight, m3-green, m2, dark, glassmorphic, pastel, ai, cyberpunk, blueprint, blueprint-dark.
main.css is required and already includes basic. The rest are separate files:
import "timepicker-ui/main.css";
import "timepicker-ui/theme-dark.css";
new TimepickerUI(input, { ui: { theme: "dark" } });
Or import timepicker-ui/index.css to get all of them. Writing your own theme: styling guide.
Range, timezone and wheel ship as separate imports. Register them once before creating pickers. You need WheelPlugin for the wheel and compact-wheel modes.
import { TimepickerUI, PluginRegistry } from "timepicker-ui";
import { RangePlugin } from "timepicker-ui/plugins/range";
import { TimezonePlugin } from "timepicker-ui/plugins/timezone";
import { WheelPlugin } from "timepicker-ui/plugins/wheel";
PluginRegistry.register(RangePlugin);
PluginRegistry.register(TimezonePlugin);
PluginRegistry.register(WheelPlugin);
new TimepickerUI(input, { range: { enabled: true } });
From 4.5 nothing breaks. 4.6 adds ui.inline.embedded, range.linked, clock.isTimeDisabled with refreshDisabledTime(), the setRange() / getRange() / setActivePart() methods, and a remover returned by on() / once(). See the CHANGELOG.
From 4.4, 4.5 adds optional seconds (clock.showSeconds) and fixes many bugs. It also renames many options to clearer names, for example ui.initialView instead of ui.mobile, and adds setOptions() in place of update(). The old names keep working until 5.0, and the options page lists every pair. A few changes can affect an existing setup: the AM/PM part of the value is always AM / PM whatever labels.am / labels.pm say, out-of-range clock.disabledTime.ranges edges such as 25:00 are reported as errors, the old .tp-ui-ripple classes are gone, and some focus-ring and radius tokens changed. The CHANGELOG lists them under “Check before upgrading”.
From 4.3 nothing breaks. 4.4 adds the blueprint and blueprint-dark themes, 16 new labels.* for screen-reader text, and Home/End/PageUp/PageDown keys. See the CHANGELOG.
From 3.x, options are now grouped (clock, ui, labels, …), events go through picker.on(), and CSS class names changed. There’s a migration guide.
From 2.x, you have to import the CSS yourself and some event names changed (legacy guide).
React:
import { useEffect, useRef } from "react";
import { TimepickerUI } from "timepicker-ui";
import "timepicker-ui/main.css";
function TimePicker() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!ref.current) return;
const picker = new TimepickerUI(ref.current);
picker.create();
return () => picker.destroy();
}, []);
return <input ref={ref} />;
}
There’s also a React wrapper (docs). Vue and Angular examples are in the quick start docs.
datepicker-ui is the companion calendar from the same author. It has no dependencies, follows Material Design 3, is safe to import during server rendering and uses the same theme names, so the two pickers match. It covers single dates, ranges and multiple dates.
npm install @pglejzer/datepicker-ui
The date and time guide shows how to use both pickers in one form.
5.0.0 is planned but has no release date yet. 4.5.0 prepares for it. Planned changes:
PluginRegistry.register(...) and the exported plugin types (PluginRegistry, Plugin, PluginManager, PluginFactory, TemplateProvider, ClearHandler, CoreState, EventEmitter) will change. The built-in range, timezone and wheel plugins stay. A migration guide ships with 5.0.tp-ui- prefix, but the part after it is reworked to follow Material Design component naming. Custom CSS that targets these classes will need updating. 5.0 ships a table of old and new names.ui.mobile (now ui.initialView) and clock.smoothHourSnap (now clock.discreteHourJump). The options page lists every pair. The select:am / select:pm events go too (use select:period), and so does update() (use setOptions()).behavior.returnFocus becomes true by default: on close, focus goes back to the input, or to the data-open element that opened the picker. Set behavior.returnFocus: true now to return focus to the input.OptionTypes (use TimepickerOptions)clock.currentTime.localesgetValue().degreesHours and getValue().degreesMinuteslabels.toggleLabel, labels.format24Label, labels.timezoneSelectorLabelfocusInputAfterCloseModal option (use behavior.returnFocus)hide event and HideEventData (never emitted)range:get-disabled-time and range:update-disabled events (never emitted)ErrorEventData.rejectedHour and rejectedMinute (always undefined)TimepickerEventMap (range:minute:commit, animation:*) and its catch-all index signature, so on('typo') becomes a type errorbehavior.delayHandler (it only delays closing and never debounced buttons)TimepickerUI.isAvailable() (same as document.querySelector(...) !== null)hour, minutes, okButton, cancelButton, clockHandopen(cb) and destroy() in both forms, destroy(cb) and destroy({ callback }) (use once('open') or run the code after the call)RangeManager, TimezoneManager and WheelManager exported from the plugin entry points (you can’t get an instance today, and the new plugin API replaces them)select:hour, select:minute and select:second fire once when a value of their unit is picked, in every mode. Today, in clock mode, they fire when a field is activated, so picking an hour fires select:minute. To follow face changes, use switch:face, which 4.5.0 already emitsui.footer replaces ui.inline.showButtons and wheel.hideFooter: false hides OK, Cancel and Clear in every mode, and true shows them in inline mode toobehavior.commitOnChange replaces ui.inline.autoUpdate and wheel.commitOnScroll: each picked value is written to the input and emits confirm, and the picker stays openwheel.ignoreOutsideClick moves to behavior.ignoreOutsideClick and works in every mode, the clock modal includedlabels.minute, so its default changes from Minutes to Minuteui.cssClass is applied to one element instead of twoPluginRegistry, so they could not be registered from a <script> tag). The core UMD build stays--tp-tertiary-container, --tp-on-tertiary-container, --tp-radius, --tp-hover-bg and 18 of the 34 --tp-size-* tokens. 5.0 lists them all./plugins/* wildcard in package.json exports is removed, while ./plugins/range, ./plugins/timezone and ./plugins/wheel staycrane-straight becomes a variant of crane with square corners (option name not decided)ui.keyboardIcon / ui.clockIcon take an element or a factory function instead of an HTML stringDetails are on the roadmap.
See app/README.md.
Issues and PRs welcome on GitHub.
MIT © Piotr Glejzer