Server Rendering
Create the picker in the browser and destroy it when the component unmounts.
You can import timepicker-ui on the server: loading the package does not touch window or document. The picker itself needs the DOM, so create it in a lifecycle hook that runs only in the browser, and call destroy() when the component unmounts.
On the server, new TimepickerUI() throws TimepickerUI requires browser environment. For ReferenceError: document is not defined, see the Troubleshooting entry.
Next.js
#In the App Router, pages and layouts are Server Components by default. Put the picker in a file that starts with 'use client' and create it in useEffect, which runs only in the browser:
'use client';import { useEffect, useRef } from 'react';import { TimepickerUI } from 'timepicker-ui';import 'timepicker-ui/main.css';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" placeholder="Select time" /></div>);}
The tp-ui wrapper
Given a bare input, the picker wraps it in a new div.tp-ui, and destroy() leaves that div in place. React does not know about it. When the component unmounts while its parent stays on the page, React calls removeChild on the parent it rendered the input into, and the call throws. With div.tp-ui in your JSX, the picker uses that element as its root and moves nothing.
With timepicker-ui-react
The Timepicker component creates and destroys the picker for you. It uses hooks, and the package does not add 'use client', so import it from a file that does:
'use client';import { Timepicker } from 'timepicker-ui-react';import 'timepicker-ui/main.css';export function TimeField() {return (<Timepickerplaceholder="Select time"onConfirm={(data) => console.log(data.hour, data.minutes)}/>);}
Any page or layout can render <TimeField />. Keep callbacks such as onConfirm in the client file: props passed from a Server Component must be serializable, and functions are not. Import timepicker-ui/main.css here or once in app/layout.tsx. The React section lists the component's props.
Nuxt
#Vue does not run onMounted during server rendering. Load the library inside it and destroy the picker in onBeforeUnmount:
<script setup lang="ts">import { onBeforeUnmount, onMounted, ref } from 'vue';import type { TimepickerUI } from 'timepicker-ui';const input = ref<HTMLInputElement | null>(null);let picker: TimepickerUI | undefined;onMounted(async () => {const { TimepickerUI } = await import('timepicker-ui');if (!input.value) return;picker = new TimepickerUI(input.value);picker.create();});onBeforeUnmount(() => picker?.destroy());</script><template><input ref="input" type="text" placeholder="Select time" /></template>
A static import works too, because importing the package is safe on the server.
Load the styles for the whole app in nuxt.config.ts, or import timepicker-ui/main.css in the component:
export default defineNuxtConfig({css: ['timepicker-ui/main.css'],});
With <ClientOnly>
<ClientOnly> renders its #fallback slot on the server and its content only in the browser. Here the fallback is a disabled field that stays until <ClientOnly> mounts:
<template><ClientOnly><TimeField /><template #fallback><input type="text" placeholder="Select time" disabled /></template></ClientOnly></template>
SvelteKit
#Svelte does not run onMount during server rendering. Load the library inside it and return a cleanup function, which Svelte calls when the component is destroyed:
<script>import { onMount } from 'svelte';import 'timepicker-ui/main.css';let input;onMount(() => {let picker;import('timepicker-ui').then(({ TimepickerUI }) => {picker = new TimepickerUI(input);picker.create();});return () => picker?.destroy();});</script><input bind:this={input} type="text" placeholder="Select time" />
The callback uses .then() instead of async. An async function returns a promise, and Svelte only calls a cleanup function that onMount returns synchronously.
Plugins and Themes
#Register plugins in the same client code, before the first new TimepickerUI(). PluginRegistry.register() only stores the plugin and skips a name that is already registered, so calling it at the top of a client module is safe during server rendering:
'use client';import { useEffect, useRef } from 'react';import { PluginRegistry, TimepickerUI } from 'timepicker-ui';import { WheelPlugin } from 'timepicker-ui/plugins/wheel';PluginRegistry.register(WheelPlugin);export function WheelField() {const inputRef = useRef<HTMLInputElement>(null);useEffect(() => {if (!inputRef.current) return;const picker = new TimepickerUI(inputRef.current, { ui: { mode: 'wheel', theme: 'dark' } });picker.create();return () => picker.destroy();}, []);return (<div className="tp-ui"><input ref={inputRef} type="text" placeholder="Select time" /></div>);}
With a dynamic import, load the plugin next to the library. In the Nuxt component above, onMounted becomes:
onMounted(async () => {const { TimepickerUI, PluginRegistry } = await import('timepicker-ui');const { WheelPlugin } = await import('timepicker-ui/plugins/wheel');PluginRegistry.register(WheelPlugin);if (!input.value) return;picker = new TimepickerUI(input.value, { ui: { mode: 'wheel' } });picker.create();});
Theme files are global CSS. Import main.css and each theme you use once: in app/layout.tsx (Next.js), in the css list of nuxt.config.ts (Nuxt) or in the root +layout.svelte (SvelteKit). Then pick the theme with ui.theme.
import 'timepicker-ui/main.css';import 'timepicker-ui/theme-dark.css';
See Themes for the theme names and Plugins for the options of each plugin.