{"id":17087828,"url":"https://github.com/dan-lee/timescape","last_synced_at":"2025-04-08T08:11:25.621Z","repository":{"id":175292124,"uuid":"644769420","full_name":"dan-lee/timescape","owner":"dan-lee","description":"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.","archived":false,"fork":false,"pushed_at":"2024-12-25T11:05:59.000Z","size":1839,"stargazers_count":180,"open_issues_count":4,"forks_count":7,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-01T05:33:38.414Z","etag":null,"topics":["date","headless","input","react","solidjs","svelte","time","vue"],"latest_commit_sha":null,"homepage":"https://timescape.daniellehr.de","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dan-lee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-24T08:10:32.000Z","updated_at":"2025-03-24T16:41:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"ce38043f-881b-4797-aa1c-e3ecb2c14e14","html_url":"https://github.com/dan-lee/timescape","commit_stats":{"total_commits":98,"total_committers":6,"mean_commits":"16.333333333333332","dds":"0.30612244897959184","last_synced_commit":"f84d2a07e4e765b42d7263c0a82cfd2965e29403"},"previous_names":["dan-lee/timescape"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dan-lee%2Ftimescape","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dan-lee%2Ftimescape/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dan-lee%2Ftimescape/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dan-lee%2Ftimescape/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dan-lee","download_url":"https://codeload.github.com/dan-lee/timescape/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247801154,"owners_count":20998338,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["date","headless","input","react","solidjs","svelte","time","vue"],"created_at":"2024-10-14T13:35:03.950Z","updated_at":"2025-04-08T08:11:25.589Z","avatar_url":"https://github.com/dan-lee.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# timescape\n\nA powerful, headless library that elegantly fills the void left by HTML's native [`\u003cinput type=\"time\"\u003e`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time) and [`\u003cinput type=\"date\"\u003e`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date).\n\n`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.\n\nKey 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.\n\n\u003cimg src=\"./assets/timescape.apng\" style=\"max-height:120px\" /\u003e\n\nSee [Storybook](https://timescape.daniellehr.de) or [check out the examples](#examples) of how to use it + [StackBlitz ⚡](https://stackblitz.com/@dan-lee/collections/timescape) for more demonstrations.\n\n\u003ca href=\"https://stellate.co\" target=\"_blank\"\u003e\n  \u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/dan-lee/timescape/main/assets/badge-dark.svg\" /\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/dan-lee/timescape/main/assets/badge-light.svg\" alt=\"Sponsored by Stellate\" /\u003e\n  \u003c/picture\u003e\n\u003c/a\u003e\n\n## Features\n\n- **🧢 Headless Architecture**: You control the UI – `timescape` handles the logic.\n- **🧩 Framework Compatibility**: Adapters for [React](https://react.dev/), [Preact](https://preactjs.com/), [Vue](https://vuejs.org/), [Svelte](https://svelte.dev/), and [Solid](https://www.solidjs.com/).\n- **⚙ 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.\n- **👥 Accessibility**: Full A11y compliance, keyboard navigation and manual input.\n- **⏰ Date and time flexibility**: Supports min/max dates and 24/12 hour clock formats.\n- **🪶 Lightweight**: No external dependencies.\n- **🔀 Enhanced input fields**: A supercharged `\u003cinput type=\"date/time\"\u003e`, offering additional flexibility.\n- **🤳 Touch device support**: Use it on any device, including touch devices.\n\n## Installation\n\n```shell\n# pnpm\npnpm add timescape\n\n# yarn\nyarn add timescape\n\n# npm\nnpm install --save timescape\n```\n\n## Examples\n\n\u003cdetails open\u003e\n  \u003csummary\u003e\u003cstrong\u003eReact\u003c/strong\u003e\u003c/summary\u003e\n\n[Edit on StackBlitz ⚡](https://stackblitz.com/edit/timescape-react?file=src%2FApp.tsx)\n\n```tsx\nimport { useTimescape } from \"timescape/react\";\n\nfunction App() {\n  const { getRootProps, getInputProps, options, update } = useTimescape({\n    date: new Date(),\n    onChangeDate: (nextDate) =\u003e {\n      console.log(\"Date changed to\", nextDate);\n    },\n  });\n\n  // To change any option:\n  // update((prev) =\u003e ({ ...prev, date: new Date() }))\n\n  return (\n    \u003cdiv className=\"timescape\" {...getRootProps()}\u003e\n      \u003cinput {...getInputProps(\"days\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"months\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"years\")} /\u003e\n      \u003cspan\u003e \u003c/span\u003e\n      \u003cinput {...getInputProps(\"hours\")} /\u003e\n      \u003cspan\u003e:\u003c/span\u003e\n      \u003cinput {...getInputProps(\"minutes\")} /\u003e\n      \u003cspan\u003e:\u003c/span\u003e\n      \u003cinput {...getInputProps(\"seconds\")} /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ePreact\u003c/strong\u003e\u003c/summary\u003e\n\n[Edit on StackBlitz ⚡](https://stackblitz.com/edit/timescape-preact?file=src%2Fapp.tsx)\n\nThis package uses Preact signals, if you want to use it without just use the React implementation in compat mode.\n\n```tsx\nimport { effect } from \"@preact/signals\";\nimport { useTimescape } from \"timescape/preact\";\n\nfunction App() {\n  const { getRootProps, getInputProps, options } = useTimescape({\n    date: new Date(),\n  });\n\n  effect(() =\u003e {\n    console.log(\"Date changed to\", options.value.date);\n  });\n\n  // To change any option:\n  // options.value = { ...options.value, date: new Date() }\n\n  return (\n    \u003cdiv className=\"timescape\" {...getRootProps()}\u003e\n      \u003cinput {...getInputProps(\"years\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"months\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"days\")} /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eVue\u003c/strong\u003e\u003c/summary\u003e\n\n[Edit on StackBlitz ⚡](https://stackblitz.com/edit/timescape-vue?file=src%2FApp.vue)\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"timescape\" :ref=\"registerRoot()\"\u003e\n    \u003cinput :ref=\"registerElement('years')\" /\u003e\n    \u003cspan\u003e/\u003c/span\u003e\n    \u003cinput :ref=\"registerElement('months')\" /\u003e\n    \u003cspan\u003e/\u003c/span\u003e\n    \u003cinput :ref=\"registerElement('days')\" /\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Change any option --\u003e\n  \u003cbutton @click=\"options.date = new Date()\"\u003eChange date\u003c/button\u003e\n\u003c/template\u003e\n\n\u003cscript lang=\"ts\" setup\u003e\nimport { type UseTimescapeOptions, useTimescape } from \"timescape/vue\";\nimport { watchEffect } from \"vue\";\n\nconst { registerElement, registerRoot, options } = useTimescape({\n  date: new Date(),\n});\n\nwatchEffect(() =\u003e {\n  console.log(\"Date changed to\", options.value.date);\n});\n\u003c/script\u003e\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSvelte\u003c/strong\u003e\u003c/summary\u003e\n\n[Edit on StackBlitz ⚡](https://stackblitz.com/edit/timescape-svelte?file=src%2FApp.svelte)\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\nimport { derived } from \"svelte/store\";\nimport { createTimescape } from \"timescape/svelte\";\n\nconst { inputProps, rootProps, options } = createTimescape({\n  date: new Date(),\n});\n\nconst date = derived(options, ($o) =\u003e $o.date);\n\ndate.subscribe((nextDate) =\u003e {\n  console.log(\"Date changed to\", nextDate);\n});\n\n// To change any option:\n// options.update((prev) =\u003e ({ ...prev, date: new Date() }))\n\u003c/script\u003e\n\n\u003cdiv class=\"timescape\" use:rootProps\u003e\n  \u003cinput use:inputProps={'days'} /\u003e\n  \u003cspan\u003e/\u003c/span\u003e\n  \u003cinput use:inputProps={'months'} /\u003e\n  \u003cspan\u003e/\u003c/span\u003e\n  \u003cinput use:inputProps={'years'} /\u003e\n\u003c/div\u003e\n\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSolid\u003c/strong\u003e\u003c/summary\u003e\n\n[Edit on StackBlitz ⚡](https://stackblitz.com/edit/timescape-solid?file=src%2FApp.tsx)\n\n```tsx\nimport { createEffect } from \"solid-js\";\nimport { useTimescape } from \"timescape/solid\";\n\nfunction App() {\n  const { getInputProps, getRootProps, options, update } = useTimescape({\n    date: new Date(),\n  });\n\n  createEffect(() =\u003e {\n    console.log(\"Date changed to\", options.date);\n  });\n\n  // To change any option:\n  // update('date', new Date())\n  // or update({ date: new Date() })\n\n  return (\n    \u003cdiv class=\"timescape\" {...getRootProps()}\u003e\n      \u003cinput {...getInputProps(\"years\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"months\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...getInputProps(\"days\")} /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eVanilla JS\u003c/strong\u003e\u003c/summary\u003e\n\n```tsx\nimport { TimescapeManager } from \"timescape\";\n\nconst container = document.createElement(\"div\");\ndocument.body.appendChild(container);\n\ncontainer.innerHTML = ` \n  \u003cdiv class=\"timescape\" id=\"timescape-root\"\u003e\n    \u003cinput data-type=\"days\" placeholder=\"dd\" /\u003e\n    \u003cspan\u003e/\u003c/span\u003e\n    \u003cinput data-type=\"months\" placeholder=\"mm\" /\u003e\n    \u003cspan\u003e/\u003c/span\u003e\n    \u003cinput data-type=\"years\" placeholder=\"yyyy\" /\u003e\n  \u003c/div\u003e\n`;\n\nconst timeManager = new TimescapeManager();\n\ntimeManager.date = new Date();\n\ntimeManager.subscribe((nextDate) =\u003e {\n  console.log(\"Date changed to\", nextDate);\n});\n\ntimeManager.registerRoot(document.getElementById(\"timescape-root\")!);\n\ntimeManager.registerElement(\n  container.querySelector('[data-type=\"days\"]')!,\n  \"days\",\n);\ntimeManager.registerElement(\n  container.querySelector('[data-type=\"months\"]')!,\n  \"months\",\n);\ntimeManager.registerElement(\n  container.querySelector('[data-type=\"years\"]')!,\n  \"years\",\n);\n```\n\n\u003c/details\u003e\n\n## Options\n\nThe 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).\n\n```tsx\ntype Options = {\n  date?: Date;\n  minDate?: Date | $NOW; // see more about $NOW below\n  maxDate?: Date | $NOW;\n  hour12?: boolean;\n  wrapAround?: boolean;\n  digits?: \"numeric\" | \"2-digit\";\n  snapToStep?: boolean;\n  wheelControl?: boolean;\n  disallowPartial?: boolean\n};\n```\n\n| Option            | Default     | Description                                                                                                                                                                                                                                                                                                                                                      |\n| ----------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `date`            | `undefined` | The initial date. If not set, it will render the placeholders in their respective input fields (if set).                                                                                                                                                                                                                                                         |\n| `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](#now-vavue)                                                                                                                                                                                                                     |\n| `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](#now-value)                                                                                                                                                                                                                     |\n| `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.                                                                                                                                                                                                                                        |\n| `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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#day) convention. |\n| `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.                                                                                                                                                                                                                                        |\n| `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](#step-on-input-elements)                                                                                                                                                      |\n| `wheelControl`    | `false`     | If set to `true`, the user can use the mouse wheel or touchpad to increment/decrement values.                                                                                                                                                                                                                                                                    |\n| `disallowPartial` | `false`     | If `true`, the input requires fully completed dates and times. By default partial dates are allowed, similar to native HTML input behavior.                                                                                                                                                                                                                      |\n\n### `$NOW` value\n\n`$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.\n\n`$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.\"\n\nIt can be imported from the package like so:\n\n```ts\nimport { $NOW } from \"timescape\";\n\n// or from a specific module\nimport { $NOW } from \"timescape/react\";\n\n// Svelte import names prohibit a $ prefix, so it's renamed to NOW there\nimport { NOW } from \"timescape/svelte\";\n```\n\n### `placeholder` on input elements\n\nThe `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`\n\n### `step` on input elements\n\nThe [`step` attribute for input elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/step) 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`](#options) if you want to snap to the nearest step.\n\n### Preventing default `keydown` behavior\n\nBy 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`:\n\n```tsx\n\u003cinput\n  onKeyDownCapture={(e) =\u003e {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n    }\n  }}\n/\u003e\n```\n\n## Ranges\n\n`timescape` supports ranges for the date/time inputs. This means a user can select a start and end. This is useful for things like booking systems, where you want to allow the user to select a range of dates.\n\nThis is achieved by using two `timescape` instances, one for the start and one for the end. You can set their options independently, and they return the respective options and update functions in the `from` and `to` objects.\n\nExample usage (this works similar for all supported libraries):\n\n```tsx\nimport { useTimescapeRange } from \"timescape/react\";\n// Use `createTimescapeRange` for Svelte\n\nconst { getRootProps, from, to } = useTimescapeRange({\n  from: { date: new Date(\"2000-01-01\") },\n  to: { date: new Date() },\n});\n\nreturn (\n  \u003cdiv {...getRootProps()}\u003e\n    \u003cdiv\u003e\n      \u003cinput {...from.getInputProps(\"days\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...from.getInputProps(\"months\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...from.getInputProps(\"years\")} /\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n      \u003cinput {...to.getInputProps(\"days\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...to.getInputProps(\"months\")} /\u003e\n      \u003cspan\u003e/\u003c/span\u003e\n      \u003cinput {...to.getInputProps(\"years\")} /\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n);\n```\n\n## Anatomy \u0026 styling\n\nThe component is designed to be as un-opinionated as possible, so it doesn't come with any styling out of the box. You can style it however you want, but here are some tips to get you started.\n\nThis is how it could look like:\n\n\u003cimg src=\"https://github.com/dan-lee/timescape/assets/571589/bac69b8c-e108-43db-8203-2dcbdb5030eb\" height=\"250\" /\u003e\n\nA typical anatomy of a timescape component may look like this:\n\n### HTML\n\n```html\n\u003cdiv class=\"timescape\"\u003e\n  \u003c!-- Date inputs --\u003e\n  \u003cinput /\u003e\n  \u003cspan class=\"separator\"\u003e/\u003c/span\u003e\n  \u003cinput /\u003e\n  \u003cspan class=\"separator\"\u003e/\u003c/span\u003e\n  \u003cinput /\u003e\n\n  \u003cspan class=\"separator\"\u003e\u0026nbsp;\u003c/span\u003e\n\n  \u003c!-- Time inputs --\u003e\n  \u003cinput /\u003e\n  \u003cspan class=\"separator\"\u003e:\u003c/span\u003e\n  \u003cinput /\u003e\n  \u003cspan class=\"separator\"\u003e:\u003c/span\u003e\n  \u003cinput /\u003e\n\u003c/div\u003e\n```\n\n### CSS\n\n```css\n/**\n * Root element\n */\n.timescape {\n  display: flex;\n  align-items: center;\n  gap: 1px;\n  width: fit-content;\n  border: 1px solid #b2b2b2;\n  padding: 5px;\n  user-select: none;\n  border-radius: 10px;\n}\n\n.timescape:focus-within {\n  outline: 1px solid #8f47d4;\n  border-color: #8f47d4;\n}\n\n/**\n * Date and time input elements\n */\n.timescape input {\n  /* This is an important style, as it ensures that the inputs have\n  the same width regardless of the number of characters they contain. */\n  font-variant-numeric: tabular-nums;\n  height: fit-content;\n  /* These are handled by the `:focus` selector */\n  border: none;\n  outline: none;\n  cursor: default;\n  user-select: none;\n  box-sizing: content-box;\n  /* For touch devices where input fields are not set to readonly */\n  caret-color: transparent;\n\n  /* For the calculation of the input width these are important */\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n.timescape input:focus {\n  background-color: #8f47d4;\n  color: #fff;\n  border-radius: 6px;\n  padding: 2px;\n}\n\n/**\n * Separator elements\n */\n.timescape .separator {\n  font-size: 80%;\n  color: #8c8c8c;\n  margin: 0;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdan-lee%2Ftimescape","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdan-lee%2Ftimescape","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdan-lee%2Ftimescape/lists"}