A flexible, headless date and time input library for JavaScript. Provides tools for building fully customizable date and time input fields, with support for libraries like React, Preact, Vue, Svelte and Solid.
npm install timescapeA powerful, headless library that elegantly fills the void left by HTML's native and .
timescape is a toolkit for creating custom date and time input components. It helps you handle date and time data easily while giving you full control over the design and presentation. timescape supports multiple libraries, including React, Vue, Preact, Svelte, Solid, and native JavaScript.
Key features such as accessibility and keyboard navigation are at the core of timescape, allowing you to focus on creating user-centric date and time inputs that integrate seamlessly into your projects.
See Storybook or check out the examples of how to use it + StackBlitz ⚡ for more demonstrations.
- 🧢 Headless Architecture: You control the UI – timescape handles the logic.
- 🧩 Framework Compatibility: Adapters for React, Preact, Vue, Svelte, and Solid.
- ⚙ Flexible API: Hooks (or equivalents) return getters for seamless component integration. Order of inputs (i.e. format) is completely up to you by just rendering in the order you prefer.
- 👥 Accessibility: Full A11y compliance, keyboard navigation and manual input.
- ⏰ Date and time flexibility: Supports min/max dates and 24/12 hour clock formats.
- 🪶 Lightweight: No external dependencies.
- 🔀 Enhanced input fields: A supercharged , offering additional flexibility.
- 🤳 Touch device support: Use it on any device, including touch devices.
``shellpnpm
pnpm add timescape
Examples
React
`tsx
import { useTimescape } from "timescape/react";function App() {
const { getRootProps, getInputProps, options, update } = useTimescape({
date: new Date(),
onChangeDate: (nextDate) => {
console.log("Date changed to", nextDate);
},
});
// To change any option:
// update((prev) => ({ ...prev, date: new Date() }))
return (
/
/
:
:
);
}
`
Preact
This package uses Preact signals, if you want to use it without just use the React implementation in compat mode.
`tsx
import { effect } from "@preact/signals";
import { useTimescape } from "timescape/preact";function App() {
const { getRootProps, getInputProps, options } = useTimescape({
date: new Date(),
});
effect(() => {
console.log("Date changed to", options.value.date);
});
// To change any option:
// options.value = { ...options.value, date: new Date() }
return (
/
/
);
}
`
Vue
`vue
/
/
`
Svelte
`svelte
/
/
`
Solid
`tsx
import { createEffect } from "solid-js";
import { useTimescape } from "timescape/solid";function App() {
const { getInputProps, getRootProps, options, update } = useTimescape({
date: new Date(),
});
createEffect(() => {
console.log("Date changed to", options.date);
});
// To change any option:
// update('date', new Date())
// or update({ date: new Date() })
return (
/
/
);
}
`
Vanilla JS
`tsx
import { TimescapeManager } from "timescape";const container = document.createElement("div");
document.body.appendChild(container);
container.innerHTML =
;const timeManager = new TimescapeManager();
timeManager.date = new Date();
timeManager.subscribe((nextDate) => {
console.log("Date changed to", nextDate);
});
timeManager.registerRoot(document.getElementById("timescape-root")!);
timeManager.registerElement(
container.querySelector('[data-type="days"]')!,
"days",
);
timeManager.registerElement(
container.querySelector('[data-type="months"]')!,
"months",
);
timeManager.registerElement(
container.querySelector('[data-type="years"]')!,
"years",
);
`Options
The options passed to
timescape are the _initial values_. timescape returns the options either as store/signal or with an updater function (depending on the library you are using).`tsx
type Options = {
date?: Date;
minDate?: Date | $NOW; // see more about $NOW below
maxDate?: Date | $NOW;
hour12?: boolean;
wrapAround?: boolean;
digits?: "numeric" | "2-digit";
snapToStep?: boolean;
wheelControl?: boolean;
disallowPartial?: boolean
};
`| Option | Default | Description |
| ----------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
date | undefined | The initial date. If not set, it will render the placeholders in their respective input fields (if set). |
| minDate | undefined | The minimum date that the user can select. $NOW is a special value that represents the current date and time. See more below |
| maxDate | undefined | The maximum date that the user can select. $NOW is a special value that represents the current date and time. See more below |
| hour12 | false | If set to true, the time input will use a 12-hour format (with AM/PM). If set to false, it will use a 24-hour format. |
| digits | '2-digit' | Controls the display of the day and month in the date input. 'numeric' displays as 1-12 for month and 1-31 for day, while '2-digit' displays as 01-12 for month and 01-31 for day. This follows Intl.DateTimeFormat convention. |
| wrapAround | false | If set to true, the time input will wrap around from the end of one period (AM/PM or day) to the beginning of the next. |
| snapToStep | false | If set to true, the input value will snap to the nearest step when the user uses arrow keys to increment/decrement values. Can be further adjust by using the step attribute |
| wheelControl | false | If set to true, the user can use the mouse wheel or touchpad to increment/decrement values. |
| disallowPartial | false | If true, the input requires fully completed dates and times. By default partial dates are allowed, similar to native HTML input behavior. |$3
$NOW is a convenience value you can use for minDate and maxDate. It represents the current date and time at the moment of the user's interaction, dynamically adjusting to always reflect the current datetime value. This means you don't need to manually update it, as it always keeps itself current.$NOW is exported as a constant for better type safety. By doing so, it eliminates the need for casting it as const, which would be required if $NOW were simply a string."It can be imported from the package like so:
`ts
import { $NOW } from "timescape";// or from a specific module
import { $NOW } from "timescape/react";
// Svelte import names prohibit a $ prefix, so it's renamed to NOW there
import { NOW } from "timescape/svelte";
`$3
The
placeholder attribute on the input elements is supported and will be used to display the placeholder text. Usually it's to indicate the expected format of the input, e.g. yyyy/mm/dd$3
step attribute for input elements is supported and will be used to increment/decrement the values when the user uses the arrow keys. The default value is 1, but you can set it to any value you want. Also see snapToStep if you want to snap to the nearest step.$3
By default, timescape intercepts keydown events to enhance input behavior. If you want to handle keydown events yourself and prevent the default processing, you can do so by attaching your event handler during the capturing phase and calling
preventDefault:`tsx
onKeyDownCapture={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
}
}}
/>
`Custom AM/PM Controls
While
timescape provides getInputProps("am/pm") for a standard input field, you may want to use custom controls like select dropdowns, buttons, or checkboxes for AM/PM selection. All hooks/functions return an ampm object with the following methods:`tsx
ampm.value // Current value: "am" | "pm" | undefined
ampm.set(value) // Set to "am" or "pm"
ampm.toggle() // Toggle between AM and PM
ampm.getSelectProps() // Returns props for binding to a