Troubleshooting
Find the symptom you see, then apply the fix.
Setup
#The picker opens but has no styles
Fix:
- Import
timepicker-ui/main.cssonce, for example in your app's entry file. It contains the core styles and the defaultbasictheme. - For another theme, also import
timepicker-ui/theme-<name>.cssand setui.themeto the same name.index.tsimport { TimepickerUI } from 'timepicker-ui';import 'timepicker-ui/main.css';import 'timepicker-ui/theme-dark.css';const picker = new TimepickerUI('#time', { ui: { theme: 'dark' } });picker.create();
Why: The package ships its CSS as separate files, so you load only the themes you use. timepicker-ui/index.css contains the core styles and all 12 themes in one file.
Nothing happens when the input is clicked
Fix:
- Call
create()afternew TimepickerUI(...). The handlers that open the picker on click and on Enter are added bycreate(), not by the constructor.index.tsimport { TimepickerUI } from 'timepicker-ui';const picker = new TimepickerUI('#time');picker.create(); - If the console shows
Input element not found, the selector matched nothing when the code ran, or the element has no<input>inside it. Run the code after the element is in the page.
A range, timezone or wheel option is ignored, or an error event fires
Fix:
- Import the plugin from
timepicker-ui/plugins/<name>and pass it toPluginRegistry.register()once, before the firstnew TimepickerUI(...).index.tsimport { TimepickerUI, PluginRegistry } from 'timepicker-ui';import { RangePlugin } from 'timepicker-ui/plugins/range';PluginRegistry.register(RangePlugin);const picker = new TimepickerUI('#time', { range: { enabled: true } });picker.create(); - A missing plugin is reported only through the
errorevent, when the picker opens. Listen to it to see the message.index.tspicker.on('error', ({ error }) => console.warn(error));
Why: Each picker sets up its plugins in its constructor, so a plugin registered after new TimepickerUI(...) does not work in that picker. Without its plugin, the picker leaves out the range and timezone controls and shows the clock instead of the wheel. With range.enabled set, OK then does not close the picker.
Styling
#ui.theme: 'my-theme' has no effect
Fix:
- Pass your class name in
ui.cssClassinstead. The picker adds the class to.tp-ui-wrapperwhen it opens.index.tsconst picker = new TimepickerUI('#time', {ui: { cssClass: 'my-theme' },});picker.create(); - Set the
--tp-*variables under.tp-ui-wrapper.my-theme. CSS Variables lists them.index.css.tp-ui-wrapper.my-theme {--tp-bg: #ffffff;--tp-text: #1d1b20;--tp-primary: #0b57d0;--tp-on-primary: #ffffff;}
Why: ui.theme selects one of the 12 built-in themes. The picker writes the name to the data-theme attribute of the modal and does not add it as a class, so a rule such as .my-theme never matches.
TypeScript rejects a custom ui.theme name
Fix: Leave ui.theme unset or use a built-in name, and pass your own class in ui.cssClass. The ui.theme entry above shows the CSS.
Why: ui.theme is typed as a union of the 12 built-in theme names.
The modal shows behind a header or inside a clipped container
Fix:
- If you set
ui.appendTo, point it at an element outside the clipped container, or remove the option so the modal goes to<body>. - The modal has
z-index: 5000. If your header has a higherz-index, lower the header's value, or raise the modal's in CSS that loads aftertimepicker-ui/main.css.index.css.tp-ui-modal {z-index: 10000;}
Why: The modal is position: fixed. Inside an ancestor with transform or filter, a fixed element is placed relative to that ancestor instead of the viewport, and its z-index only counts inside it. --tp-z-modal is defined in :root, but no rule reads it, so changing it does nothing.
My CSS does not override the built-in styles
Fix:
- Give the picker a class with
ui.cssClass, for example'my-picker'. - Start each selector with
.tp-ui-wrapper.my-picker. Built-in state rules such as.tp-ui-hour.activeand.tp-ui-hour.mobile.activeuse two or three classes, and the prefix adds two more.index.css.tp-ui-wrapper.my-picker .tp-ui-hour.active,.tp-ui-wrapper.my-picker .tp-ui-minutes.active {background-color: #111111;color: #ffffff;} - See Specificity on the CSS Classes page.
Why: A rule loses when it has fewer classes than the built-in one, or the same number and loads earlier.
Frameworks
#ReferenceError: document is not defined or TimepickerUI requires browser environment in Next.js, Nuxt or SvelteKit
Fix:
- Create the picker in a hook that runs only in the browser:
useEffectin React,onMountedin Vue and Nuxt,onMountin Svelte and SvelteKit. In the Next.js App Router, the file also needs'use client'.index.tsx'use client';import { useEffect, useRef } from 'react';import { TimepickerUI } from 'timepicker-ui';export function TimeField() {const inputRef = useRef<HTMLInputElement>(null);useEffect(() => {if (!inputRef.current) return;const picker = new TimepickerUI(inputRef.current);picker.create();return () => picker.destroy();}, []);return <div className="tp-ui"><input ref={inputRef} type="text" /></div>;} - Read
documentandwindowonly inside that hook as well. - Server Rendering shows the setup for Next.js, Nuxt and SvelteKit.
Why: Importing timepicker-ui on the server is safe, because no module reads window or document when it loads. The constructor needs a browser: it throws TimepickerUI requires browser environment when window is undefined. ReferenceError: document is not defined comes from your own code, for example a document.querySelector() call in the component body.
Two pickers open, or events fire twice, in React development mode
Fix: Return () => picker.destroy() from the effect that creates the picker.
useEffect(() => {if (!inputRef.current) return;const picker = new TimepickerUI(inputRef.current);picker.create();return () => picker.destroy();}, []);
Why: In development, Strict Mode runs each effect, then its cleanup, then the effect again. Without a cleanup, the first picker stays on the input next to the second one, and both open on click. destroy() removes the picker's listeners and its modal.
Failed to execute 'removeChild' on 'Node' when a React component unmounts
Fix: Wrap the input in an element with the tp-ui class. The picker then uses that element as its root and leaves the input where React rendered it.
return (<div className="tp-ui"><input ref={inputRef} type="text" /></div>);
Why: Given a bare <input>, the constructor moves it into a new div.tp-ui, and destroy() does not remove that div. React still expects the input in its original parent, so removing it can fail. timepicker-ui-react renders the wrapper for you.
The input value does not update React state
Fix:
- Set state in
callbacks.onConfirm. It runs when the user confirms a time, after the picker has written it to the input.index.tsxconst [time, setTime] = useState('');useEffect(() => {const input = inputRef.current;if (!input) return;const picker = new TimepickerUI(input, {callbacks: { onConfirm: () => setTime(input.value) },});picker.create();return () => picker.destroy();}, []); - With
timepicker-ui-react, passvalueand update it inonConfirm.index.tsximport { useState } from 'react';import { Timepicker } from 'timepicker-ui-react';export function TimeField() {const [time, setTime] = useState('10:00 AM');return (<Timepickervalue={time}onConfirm={({ hour, minutes, type }) => setTime(`${hour}:${minutes} ${type}`)}/>);}
Why: The picker sets input.value directly and does not dispatch input or change events, so React's onChange never runs.
Listeners or a modal stay after a route change
Fix: Call picker.destroy() in your framework's cleanup hook: the function returned from useEffect in React, onBeforeUnmount in Vue, ngOnDestroy in Angular, or the function returned from onMount in Svelte.
<script setup lang="ts">import { onBeforeUnmount, onMounted, ref } from 'vue';import { TimepickerUI } from 'timepicker-ui';const input = ref<HTMLInputElement | null>(null);let picker: TimepickerUI | undefined;onMounted(() => {picker = new TimepickerUI(input.value!);picker.create();});onBeforeUnmount(() => picker?.destroy());</script><template><div class="tp-ui"><input ref="input" type="text" /></div></template>
Why: An open modal sits in <body>, or in the ui.appendTo element, outside your component. If the route changes while the picker is open, the modal stays, and by default the page scroll stays locked. destroy() removes the modal, the picker's listeners and the scroll lock.
Plugins
#ui.mode: 'wheel' or 'compact-wheel' still shows a clock
Fix: Import WheelPlugin from timepicker-ui/plugins/wheel and register it once, before creating the picker.
import { TimepickerUI, PluginRegistry } from 'timepicker-ui';import { WheelPlugin } from 'timepicker-ui/plugins/wheel';PluginRegistry.register(WheelPlugin);const picker = new TimepickerUI('#time', { ui: { mode: 'wheel' } });picker.create();
Why: Without the plugin, the picker falls back to the clock face and emits error with the message WheelPlugin is not registered. Import and register it: PluginRegistry.register(WheelPlugin).