timepicker-ui
4.5.0GitHub
timepicker-ui
Guides / Troubleshooting

Troubleshooting

Find the symptom you see, then apply the fix.

Setup

#

The picker opens but has no styles

Fix:

  1. Import timepicker-ui/main.css once, for example in your app's entry file. It contains the core styles and the default basic theme.
  2. For another theme, also import timepicker-ui/theme-<name>.css and set ui.theme to the same name.
    index.ts
    import { 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:

  1. Call create() after new TimepickerUI(...). The handlers that open the picker on click and on Enter are added by create(), not by the constructor.
    index.ts
    import { TimepickerUI } from 'timepicker-ui';
    const picker = new TimepickerUI('#time');
    picker.create();
  2. 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:

  1. Import the plugin from timepicker-ui/plugins/<name> and pass it to PluginRegistry.register() once, before the first new TimepickerUI(...).
    index.ts
    import { 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();
  2. A missing plugin is reported only through the error event, when the picker opens. Listen to it to see the message.
    index.ts
    picker.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:

  1. Pass your class name in ui.cssClass instead. The picker adds the class to .tp-ui-wrapper when it opens.
    index.ts
    const picker = new TimepickerUI('#time', {
    ui: { cssClass: 'my-theme' },
    });
    picker.create();
  2. 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:

  1. If you set ui.appendTo, point it at an element outside the clipped container, or remove the option so the modal goes to <body>.
  2. The modal has z-index: 5000. If your header has a higher z-index, lower the header's value, or raise the modal's in CSS that loads after timepicker-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:

  1. Give the picker a class with ui.cssClass, for example 'my-picker'.
  2. Start each selector with .tp-ui-wrapper.my-picker. Built-in state rules such as .tp-ui-hour.active and .tp-ui-hour.mobile.active use 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;
    }
  3. 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:

  1. Create the picker in a hook that runs only in the browser: useEffect in React, onMounted in Vue and Nuxt, onMount in 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>;
    }
  2. Read document and window only inside that hook as well.
  3. 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.

index.tsx
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.

index.tsx
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:

  1. Set state in callbacks.onConfirm. It runs when the user confirms a time, after the picker has written it to the input.
    index.tsx
    const [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();
    }, []);
  2. With timepicker-ui-react, pass value and update it in onConfirm.
    index.tsx
    import { useState } from 'react';
    import { Timepicker } from 'timepicker-ui-react';
    export function TimeField() {
    const [time, setTime] = useState('10:00 AM');
    return (
    <Timepicker
    value={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.

index.vue
<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.

index.ts
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).

Esc