@tito10047/vanilla-js-timepicker
v0.2.0
Published
Lightweight vanilla JavaScript timepicker with no dependencies
Maintainers
Readme
@tito10047/vanilla-js-timepicker
Lightweight, dependency-free time picker for vanilla JavaScript and TypeScript.
Looking for a date picker? Check out vanilla-js-datepicker.

Features
- Zero dependencies — no jQuery, no Moment, no framework required.
- TypeScript first — written in TypeScript, ships full declaration files.
- 24h · 12h · seconds —
HH:mm,hh:mm a,HH:mm:ssformats from a singleformatstring. - Async lifecycle hooks —
onBeforeOpen,onBeforeChange, andvalidateaccept Promises. - Custom cell renderer —
renderCelllets you style and disable individual grid cells asynchronously (e.g. mark booked slots grey and non-clickable). - Fully accessible — ARIA
combobox,dialog,spinbuttonroles; full keyboard navigation. - CSS-variable theming — light, dark, and auto (system) themes; 20+ override-ready variables.
- Internationalization — built-in English, Slovak, Czech, German; register any custom locale.
- Multiple formats — ESM, CommonJS, and UMD builds.
Installation
npm install @tito10047/vanilla-js-timepickerQuick start
import { Timepicker } from '@tito10047/vanilla-js-timepicker'
import '@tito10047/vanilla-js-timepicker/dist/timepicker.css'
const tp = new Timepicker('#departure', {
format: 'HH:mm',
minuteStep: 15,
showNowButton: true,
showClearButton: true,
locale: 'en',
onChange: (value) => {
console.log('Selected:', value) // e.g. "14:30"
},
})<input id="departure" type="text" placeholder="--:--" />CDN (no build step)
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tito10047/vanilla-js-timepicker/dist/timepicker.css" />
<script type="module">
import { Timepicker } from 'https://cdn.jsdelivr.net/npm/@tito10047/vanilla-js-timepicker/dist/timepicker.esm.js'
new Timepicker('#time', { showNowButton: true })
</script>Usage examples
24-hour with min/max
new Timepicker('#work-start', {
format: 'HH:mm',
minTime: '08:00',
maxTime: '17:00',
minuteStep: 15,
onInvalid: (err) => showError(err.message),
})12-hour AM/PM
new Timepicker('#alarm', {
format: 'hh:mm a',
locale: 'en',
showConfirmButton: true,
closeOnSelect: false,
})With seconds
new Timepicker('#precise', {
format: 'HH:mm:ss',
minuteStep: 5,
secondStep: 5,
})Async validation
new Timepicker('#slot', {
validate: async (value) => {
const { available } = await fetch(`/api/slots?time=${value}`).then(r => r.json())
return available ? true : `${value} is already booked`
},
onInvalid: (err) => showError(err.message),
})Custom cell renderer — spinner view and grid view
renderCell works in both the spinner (value buttons you click to open the grid) and in the grid cells themselves. Pass a sync or async function; it receives the full formatted time and returns { className, title, clickable }.
import type { CellRenderResult } from '@tito10047/vanilla-js-timepicker'
new Timepicker('#appointment', {
format: 'HH:mm',
minuteStep: 30,
renderCell: async (time): Promise<CellRenderResult> => {
const { available } = await fetch(`/api/slots?time=${time}`).then(r => r.json())
return {
clickable: available,
className: available ? undefined : 'vtp-cell--booked',
title: available ? time : `${time} is already booked`,
}
},
})/* Applied to value buttons in the spinner and to cells in the grid */
.vtp-cell--booked {
color: #bbb;
text-decoration: line-through;
cursor: not-allowed;
}- Spinner view — called once with the current time on every update (open + arrow scroll). Class and title are applied to all value buttons. Stale results from fast scrolling are automatically discarded.
- Grid view — called once per cell in parallel via
Promise.all. Grid renders after all results arrive.
Dark theme
new Timepicker('#night', {
theme: 'dark',
})Programmatic API
const tp = new Timepicker('#tp')
await tp.setValue('09:30')
await tp.setNow()
await tp.clear()
const value = tp.getValue() // "09:30"
const date = tp.getDate() // Date | null
await tp.open()
await tp.close()
await tp.toggle()
tp.destroy()Auto-init from HTML attributes
<input data-timepicker data-timepicker-options='{"format":"HH:mm","showNowButton":true}' />
<input data-timepicker data-timepicker-options='{"format":"hh:mm a","locale":"en"}' />const pickers = Timepicker.autoInit() // initialize all [data-timepicker] inputsOptions
| Option | Type | Default | Description |
|---|---|---|---|
| format | string | 'HH:mm' | Format tokens: HH (24h), hh (12h), mm, ss, a. |
| locale | string \| LocaleConfig | 'en' | Built-in: en, sk, cs, de. Pass an object for custom. |
| value | string \| Date \| null | — | Initial value. |
| defaultValue | string \| Date \| null | — | Fallback initial value. |
| minTime | string | '' | Earliest allowed time (HH:mm). |
| maxTime | string | '' | Latest allowed time (HH:mm). |
| minuteStep | number | 5 | Spinner step for minutes. |
| secondStep | number | 1 | Spinner step for seconds. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Colour scheme. |
| showNowButton | boolean | false | Show "Now" button. |
| showClearButton | boolean | false | Show "Clear" button. |
| showConfirmButton | boolean | false | Show "Confirm" button. |
| openOnFocus | boolean | true | Open on input focus. |
| closeOnSelect | boolean | true | Close after selecting. |
| allowManualInput | boolean | true | Allow keyboard entry. |
| parseStrategy | 'right-fill' \| 'left-fill' \| 'smart' | 'right-fill' | How to interpret partial input. |
| renderCell | (time: string) => CellRenderResult \| Promise<CellRenderResult> | — | Async cell renderer. Return { clickable, className, title } per cell. |
| validate | (v: string) => boolean \| string \| Promise<...> | — | Custom validation function. |
| onBeforeOpen | () => boolean \| Promise<boolean> | — | Guard before opening. |
| onBeforeChange | (next, prev) => boolean \| Promise<boolean> | — | Guard before value commit. |
| onChange | (value, event) => void | — | Fired when value changes. |
| onInvalid | (err) => void | — | Fired when value is rejected. |
| onClose | (reason) => void | — | Fired when dropdown closes. |
See the full options reference for all options.
Events
The picker dispatches CustomEvents on the <input> element for every lifecycle step:
| Event | Payload | Cancellable |
|---|---|---|
| vtp:beforeopen | {} | Yes |
| vtp:open | { view } | No |
| vtp:close | { reason } | No |
| vtp:beforechange | { next, prev } | Yes |
| vtp:change | { value, date, prev } | No |
| vtp:input | { raw } | No |
| vtp:invalid | { code, message, value } | No |
| vtp:destroy | {} | No |
input.addEventListener('vtp:change', (e) => {
console.log((e as CustomEvent).detail.value)
})Theming
Override CSS custom properties to match your design system:
:root {
--vtp-accent: #e63946;
--vtp-bg-selected: #e63946;
--vtp-radius: 4px;
--vtp-font: 'Inter', sans-serif;
}Full list of variables: Theming guide.
Keyboard navigation
| Key | Action |
|---|---|
| ArrowUp / Down | Increment / decrement the focused column |
| Page Up / Down | Large increment / decrement |
| Home / End | Jump to min / max value |
| ArrowLeft / Right | Move between columns |
| Escape | Close the dropdown |
| Enter | Confirm / autofill |
Documentation
Full documentation: https://tito10047.github.io/vanilla-js-timepicker/
Live demo (8 interactive examples): https://tito10047.github.io/vanilla-js-timepicker/demo/
- Getting Started
- Initialization & Options
- Public API
- Events
- Time Formats
- Parse Strategies
- Theming & CSS Variables
- Internationalization
- Accessibility
- TypeScript
- Cookbook
License
MIT © 2026 tito10047
