⚠️ Upgrading from v2 to v3?
Major changes were introduced in version 3.0.
👉 Click here to view the Upgrade Guide
A modern, lightweight, and fully customizable time picker library built with TypeScript. Features Google's Material Design principles with extensive theming support and framework-agnostic architecture.
Curious how it works in practice?
👉 Click here to see live examples
- 🎨 9 Built-in Themes — Material, Crane, Dark, Glassmorphic, Cyberpunk, AI, and more
- 📱 Mobile-First Design — Responsive with touch and keyboard support
- 🚀 Framework Agnostic — Works with vanilla JS, React, Vue, Angular, and others
- 🔧 TypeScript Support — Full type definitions and IntelliSense
- 🎯 Inline Mode — Always-visible timepicker without modal overlay
- 🛠️ Rich API — Comprehensive methods and event system
- ♿ Accessible — ARIA-compliant with keyboard navigation
- 🌐 SSR Compatible — Works with Next.js, Nuxt, and other SSR frameworks
- 📦 Lightweight — Minimal footprint with tree-shaking support
This project is actively maintained and evolving. Some areas are planned for future improvements:
- ❌ No formal tests (unit/integration) — planned for future releases
- ❌ Some files are too large — will be split/refactored
- ❌ A few
any
types in the codebase — will be replaced with strict typings - ❌ No performance monitoring — planned metrics/logging in dev mode
If you're interested in contributing to any of these areas, feel free to open an issue or pull request!
npm install timepicker-ui
# or
yarn add timepicker-ui
For correct styling, make sure your app includes this global CSS rule:
*,
*::before,
*::after {
box-sizing: border-box;
}
This is a common default in most projects and is required by timepicker-ui
to avoid layout issues.
<input id="timepicker" type="text" />
import { TimepickerUI } from "timepicker-ui";
const input = document.querySelector("#timepicker");
const picker = new TimepickerUI(input);
picker.create();
const picker = new TimepickerUI(input, {
theme: "dark",
clockType: "24h",
animation: true,
backdrop: true,
});
picker.create();
import { useEffect, useRef } from "react";
import { TimepickerUI } from "timepicker-ui";
function TimePickerComponent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
const picker = new TimepickerUI(inputRef.current, {
onConfirm: (data) => {
console.log("Time selected:", data);
},
});
picker.create();
return () => picker.destroy();
}
}, []);
return <input ref={inputRef} type="text" />;
}
Option | Type | Default | Description |
---|---|---|---|
amLabel |
string |
"AM" |
Custom text for AM label |
animation |
boolean |
true |
Enable/disable open/close animations |
appendModalSelector |
string |
"" |
DOM selector to append timepicker (defaults to body) |
backdrop |
boolean |
true |
Show/hide backdrop overlay |
cancelLabel |
string |
"CANCEL" |
Text for cancel button |
clockType |
"12h" | "24h" |
"12h" |
Clock format type |
cssClass |
string |
undefined |
Additional CSS class for timepicker wrapper |
currentTime |
boolean | object |
undefined |
Set current time to input and picker |
delayHandler |
number |
300 |
Debounce delay for buttons (ms) |
disabledTime |
object |
undefined |
Disable specific hours, minutes, or intervals |
editable |
boolean |
false |
Allow manual input editing |
enableScrollbar |
boolean |
false |
Keep page scroll when picker is open |
enableSwitchIcon |
boolean |
false |
Show desktop/mobile switch icon |
focusInputAfterCloseModal |
boolean |
false |
Focus input after closing modal |
focusTrap |
boolean |
true |
Trap focus within modal |
hourMobileLabel |
string |
"Hour" |
Hour label for mobile version |
iconTemplate |
string |
Material Icons | HTML template for desktop switch icon |
iconTemplateMobile |
string |
Material Icons | HTML template for mobile switch icon |
id |
string |
undefined |
Custom ID for timepicker instance |
incrementHours |
number |
1 |
Hour increment step (1, 2, 3) |
incrementMinutes |
number |
1 |
Minute increment step (1, 5, 10, 15) |
inline |
object |
undefined |
Inline mode configuration |
minuteMobileLabel |
string |
"Minute" |
Minute label for mobile version |
mobile |
boolean |
false |
Force mobile version |
mobileTimeLabel |
string |
"Enter Time" |
Time label for mobile version |
okLabel |
string |
"OK" |
Text for OK button |
pmLabel |
string |
"PM" |
Custom text for PM label |
switchToMinutesAfterSelectHour |
boolean |
true |
Auto-switch to minutes after hour selection |
theme |
Theme | "basic" |
UI theme (see themes section) |
timeLabel |
string |
"Select Time" |
Time label for desktop version |
const picker = new TimepickerUI(input, {
inline: {
enabled: true,
containerId: "timepicker-container",
showButtons: false, // Hide OK/Cancel buttons
autoUpdate: true, // Auto-update input on change
},
});
const picker = new TimepickerUI(input, {
disabledTime: {
hours: [1, 3, 5, 8], // Disable specific hours
minutes: [15, 30, 45], // Disable specific minutes
interval: "10:00 AM - 2:00 PM", // Disable time range
},
});
Note: As of v3.0, you must import CSS styles manually. See Upgrade Guide for details.
Choose from 9 built-in themes:
Theme | Description |
---|---|
basic |
Default Material Design theme |
crane-straight |
Google Crane theme with straight edges |
crane-radius |
Google Crane theme with rounded edges |
m3 |
Material Design 3 (Material You) |
dark |
Dark mode theme |
glassmorphic |
Modern glass effect |
pastel |
Soft pastel colors |
ai |
Futuristic AI-inspired theme |
cyberpunk |
Neon cyberpunk aesthetic |
const picker = new TimepickerUI(input, {
theme: "cyberpunk",
});
Configure callback functions to handle timepicker events:
Callback | Type | Description |
---|---|---|
onOpen |
(data) => void |
Triggered when timepicker opens |
onCancel |
(data) => void |
Triggered when picker is cancelled |
onConfirm |
(data) => void |
Triggered when time is confirmed (OK clicked) |
onUpdate |
(data) => void |
Triggered during clock interaction (real-time) |
onSelectHour |
(data) => void |
Triggered when hour mode is activated |
onSelectMinute |
(data) => void |
Triggered when minute mode is activated |
onSelectAM |
(data) => void |
Triggered when AM is selected |
onSelectPM |
(data) => void |
Triggered when PM is selected |
onError |
(data) => void |
Triggered when invalid time format is detected |
interface CallbackData {
hour?: string;
minutes?: string;
type?: string; // 'AM' or 'PM'
degreesHours?: number;
degreesMinutes?: number;
error?: string; // Only for onError
// ... additional context data
}
const picker = new TimepickerUI(input, {
onConfirm: (data) => {
console.log(`Time selected: ${data.hour}:${data.minutes} ${data.type}`);
},
onCancel: (data) => {
console.log("User cancelled time selection");
},
onError: (data) => {
alert(`Invalid time format: ${data.error}`);
},
});
Listen to DOM events dispatched on the input element:
Event | Description |
---|---|
timepicker:open |
Fired when timepicker opens |
timepicker:cancel |
Fired when user cancels |
timepicker:confirm |
Fired when time is confirmed |
timepicker:update |
Fired during clock interaction |
timepicker:select-hour |
Fired when hour mode is selected |
timepicker:select-minute |
Fired when minute mode is selected |
timepicker:select-am |
Fired when AM is selected |
timepicker:select-pm |
Fired when PM is selected |
timepicker:error |
Fired when input validation fails |
const input = document.querySelector("#timepicker");
const picker = new TimepickerUI(input);
picker.create();
// Listen to events
input.addEventListener("timepicker:confirm", (e) => {
console.log("Time confirmed:", e.detail);
// e.detail contains: { hour, minutes, type, degreesHours, degreesMinutes }
});
input.addEventListener("timepicker:cancel", (e) => {
console.log("Cancelled:", e.detail);
});
input.addEventListener("timepicker:error", (e) => {
console.error("Error:", e.detail.error);
});
const picker = new TimepickerUI(input, options);
// Core methods
picker.create(); // Initialize the timepicker
picker.open(); // Open the timepicker programmatically
picker.close(); // Close the timepicker
picker.destroy(); // Destroy instance and clean up
// Value methods
picker.getValue(); // Get current time value
picker.setValue("14:30"); // Set time programmatically
// Configuration methods
picker.update({ options: newOptions }); // Update configuration
picker.getElement(); // Get the DOM element
// Instance management
TimepickerUI.getById("my-id"); // Get instance by ID
TimepickerUI.getAllInstances(); // Get all active instances
TimepickerUI.destroyAll(); // Destroy all instances
TimepickerUI.isAvailable(element); // Check if element exists
// Get current value
const currentTime = picker.getValue();
console.log(currentTime);
// Output: { hour: '14', minutes: '30', type: '', time: '14:30', degreesHours: 30, degreesMinutes: 180 }
// Set new time
picker.setValue("09:15 AM");
// Update configuration
picker.update({
options: { theme: "dark", clockType: "24h" },
create: true, // Reinitialize after update
});
// Instance management
const picker1 = new TimepickerUI("#picker1", { id: "picker-1" });
const picker2 = new TimepickerUI("#picker2", { id: "picker-2" });
// Later...
const foundPicker = TimepickerUI.getById("picker-1");
- Inline Mode: Always-visible timepicker without modal overlay
- Instance Management:
getById()
,destroyAll()
, and custom instance IDs - Callback System: Direct callback functions instead of manual event listeners
- New Themes: Added
dark
,glassmorphic
,pastel
,ai
,cyberpunk
- Enhanced API:
getValue()
,setValue()
, improveddestroy()
- SSR Compatibility: Better support for server-side rendering
- TypeScript Improvements: Complete type definitions and better IntelliSense
-
Event Names: All events now use
timepicker:
prefix -
Destroy Behavior:
.destroy()
no longer removes input from DOM -
Theme Options: Some theme names have changed
-
API Changes: Some method signatures have been updated
-
Styles are no longer auto-loaded You must now explicitly import CSS files. Use:
main.css
– core styles withbasic
theme onlyindex.css
– all styles including all themes- or import specific themes from
themes/
v2 (Old):
input.addEventListener('show', (e) => { ... });
input.addEventListener('cancel', (e) => { ... });
input.addEventListener('accept', (e) => { ... });
v3 (New):
input.addEventListener('timepicker:open', (e) => { ... });
input.addEventListener('timepicker:cancel', (e) => { ... });
input.addEventListener('timepicker:confirm', (e) => { ... });
v2 (Old):
const picker = new TimepickerUI(input);
input.addEventListener("accept", (e) => {
console.log("Time selected:", e.detail);
});
v3 (New):
const picker = new TimepickerUI(input, {
onConfirm: (data) => {
console.log("Time selected:", data);
},
});
v2 (Old):
picker.destroy(); // This removed the input from DOM
v3 (New):
picker.destroy(); // Only destroys timepicker, keeps input intact
// If you need to remove input, do it manually:
// input.remove();
v2 (Old):
// Limited theme options
theme: "basic" | "crane-straight" | "crane-radius" | "m3";
v3 (New):
// Extended theme options
theme: "basic" |
"crane-straight" |
"crane-radius" |
"m3" |
"dark" |
"glassmorphic" |
"pastel" |
"ai" |
"cyberpunk";
v3 New Feature:
const picker = new TimepickerUI(input, {
inline: {
enabled: true,
containerId: "timepicker-container",
showButtons: false,
autoUpdate: true,
},
});
v3 New Feature:
// Create with custom ID
const picker = new TimepickerUI(input, { id: "my-timepicker" });
// Later, get by ID
const foundPicker = TimepickerUI.getById("my-timepicker");
// Destroy all instances
TimepickerUI.destroyAll();
v2 (Old): Styles were bundled and automatically injected.
v3 (New): You must now import the styles yourself:
import "timepicker-ui/index.css";
import "timepicker-ui/main.css";
import "timepicker-ui/main.css"; // Required base
import "timepicker-ui/theme-dark.css"; // Or any other theme
import { useEffect, useRef } from "react";
import { TimepickerUI } from "timepicker-ui";
import "timepicker-ui/main.css";
import "timepicker-ui/theme-cyberpunk.css";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!inputRef.current) return;
const picker = new TimepickerUI(inputRef.current, {
theme: "cyberpunk",
});
picker.create();
return () => {
picker.destroy();
};
}, []);
return (
<div style={{ padding: "2rem" }}>
<h1>Timepicker UI React Demo</h1>
<input ref={inputRef} placeholder="Click me..." />
</div>
);
}
export default App;
ℹ️ Note: Don't forget to include global
box-sizing
rule in yourstyles.css
(see Global CSS Required).
<template>
<div style="padding: 2rem">
<h1>Timepicker UI – Vue Demo</h1>
<input ref="pickerInput" placeholder="Pick a time..." />
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { TimepickerUI } from "timepicker-ui";
import "timepicker-ui/main.css";
import "timepicker-ui/theme-glassmorphic.css";
const pickerInput = ref(null);
onMounted(() => {
if (!pickerInput.value) return;
const picker = new TimepickerUI(pickerInput.value, {
theme: "glassmorphic",
});
picker.create();
});
</script>
ℹ️ Note: Don't forget to include global
box-sizing
rule in yourstyles.css
(see Global CSS Required).
import {
Component,
ElementRef,
AfterViewInit,
ViewChild,
signal,
} from "@angular/core";
import { TimepickerUI } from "timepicker-ui";
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.html",
styleUrl: "./app.css",
})
export class App implements AfterViewInit {
protected readonly title = signal("timepicker-ui-demo");
@ViewChild("timepickerInput", { static: true })
inputRef!: ElementRef<HTMLInputElement>;
ngAfterViewInit(): void {
const picker = new TimepickerUI(this.inputRef.nativeElement, {
theme: "glassmorphic",
});
picker.create();
}
}
<input #timepickerInput placeholder="Select time..." />
"styles": [
"src/styles.css",
"timepicker-ui/main.css"
]
ℹ️ Note: Don't forget to include global
box-sizing
rule in yourstyles.css
(see Global CSS Required).
All development and build tooling is located in the app/
directory.
Please refer to app/README.md
for instructions on running the development server, building the library, running tests, and using the full toolchain.
MIT © Piotr Glejzer
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
- iOS Safari 12+
- Chrome Android 60+