DateTick — dates done right,
in every timezone.

An immutable, fully timezone-aware date toolkit — formatting, parsing, durations and live “time ago”. Works the same in vanilla JS, Angular, React, Vue and Node.

Immutable DST-safe timezones ~8 kB gzip TypeScript types Dependency-free live time-ago
npm install datetick

Everything you reach for, none of the gotchas

🧊
Immutable & chainable

Every method returns a new instance, so shared dates never mutate under your components.

🌍
Truly timezone-aware

Reading, setting, add, startOf and week math all resolve through any IANA zone — DST-safe, no plugins.

🔤
Token formatting & parsing

formatPattern() and parse() cover the full core, advanced and localized token tables.

Durations

duration() with locale-aware humanize(), as*() totals and ISO 8601 parsing & output.

Live time-ago

timeAgoLive() is a dependency-free self-rescheduling stream for any framework.

🪶
Tiny & typed

~8 kB gzipped, zero framework deps, ships with .d.ts declarations.

Quick start

Format & manipulate
import { DateTick } from 'datetick';

const d = new DateTick('en', 'UTC', '2026-01-31T12:00:00Z');

d.add(1, 'month').toISOString();   // 2026-02-28T12:00:00.000Z
d.formatPattern('DD/MM/YYYY');    // '31/01/2026'
d.startOf('week').formatPattern('ddd'); // 'Sun'
Parse, diff & durations
const a = DateTick.parse('25/06/2026', 'DD/MM/YYYY');
const span = a.diff('2026-01-01');  // ms

DateTick.duration(span).humanize();      // '5 months'
DateTick.duration(-3, 'day').humanize(true); // '3 days ago'

// Live updates (Angular/React/Vue):
const stop = d.timeAgoSubscribe(console.log);
stop();

Documentation

Installation

npm install datetick

Both ESM and a UMD build (browser global datetick) are shipped, with bundled TypeScript types and no runtime dependencies.

Creating a DateTick

Use the lowercase datetick() factory, or the DateTick class directly — they’re interchangeable and both immutable.

import datetick, { DateTick } from 'datetick';

// factory(date?, { locale?, timezone?, weekStartsOn? })
datetick('2026-06-25T19:23Z', { timezone: 'Asia/Tokyo' });
datetick();                          // now
datetick.utc('2026-06-25');            // timezone fixed to UTC
datetick.unix(1782760980);            // from Unix seconds
datetick.guessTimezone();             // runtime IANA timezone

// new DateTick(locale?, timezone?, date?, weekStartsOn?)
new DateTick('en', 'UTC', '2026-06-25T19:23Z', 1);

Inputs may be a Date, an ISO/parsable string, another DateTick, a parts object, or a parts array. Every calendar operation reflects the wall clock in the configured timezone, while the underlying instant stays absolute and DST-safe.

Getters & setters

Call with no argument to get a number; pass a value to set, returning a new instance.

year(value?) Full year (setting clamps the day, e.g. Feb 29 → non-leap year → Feb 28)
month(value?) Month, 011 (setting clamps the day, e.g. Mar 31 → Feb → Feb 28)
date(value?) Day of the month, 131
day(value?) Day of week, 0 (Sun) – 6 (Sat)
hour(value?) · minute(value?) Hours / minutes
second(value?) · millisecond(value?) Seconds / milliseconds
get(unit) Read any unit, incl. dayOfYear, week, isoWeek, quarter, daysInMonth
set(unit, value) Set any settable unit

Day.js-style plural aliases are also available: years, months, dates, days, hours, minutes, seconds, milliseconds (get/set) and weeks() (get). Each simply forwards to its singular counterpart.

Manipulation

add(amount, unit) Add time (millisecondyear, incl. week, quarter). Negative subtracts; month/year additions clamp the day.
subtract(amount, unit) Inverse of add
startOf(unit) · endOf(unit) Snap to the start/end of year/quarter/month/week/day/hour/minute/second
clone() Copy the instance
withDate · withLocale · withTimezone · withWeekStart Derive a new instance changing one aspect

Comparison

diff(date, unit?, precise?) Difference (default millisecond), truncated toward zero — for month/quarter/year only whole elapsed units count (Jun 15 → next Jun 14 is 0 years). Pass precise = true for the fractional value
diffCalendar(date, unit?) Calendar-boundary difference for day/week/month
isBefore(date, unit?) · isAfter(date, unit?) Strict ordering, optionally truncated to a unit
isSame(date, unit?) Equality, optionally truncated to a unit
isSameOrBefore · isSameOrAfter Inclusive ordering
isBetween(start, end, inclusivity?) Range check. inclusivity is a boolean or bracket notation '()' / '[]' / '[)' / '(]' ([ = inclusive edge, default exclusive)
isToday() / isTomorrow() / isYesterday() Relative calendar-day checks
isValid() Whether the wrapped date is valid

All date inputs accept a Date, a string, another DateTick, a parts object, or a parts array.

Relative time

Locale-aware “time ago” strings via Intl.RelativeTimeFormat, plus a dependency-free live stream.

d.timeAgo();                       // '3 minutes ago'
d.fromNow();                       // same, Day.js-style
d.from(other); d.to(other); d.toNow();
d.calendar();                      // 'Today at 7:23 PM'

// Live, self-rescheduling stream — works in any framework
const stop = d.timeAgoSubscribe((label) => render(label));

// Tune when it steps up from one unit to the next
d.timeAgo(undefined, { second: 120 }); // keep seconds up to 119

timeAgo(options?, thresholds?) accepts an optional { second, minute, hour, day, month } threshold override (defaults reproduce 60 / 60 / 24 / 30 / 12); the same thresholds flow through timeAgoLive() and timeAgoSubscribe().

Calendar info

dayOfYear(), week(), weekYear(), weeksInYear(), isoDay(), isoWeek(), isoWeekYear(), isoWeeksInYear(), quarter(), daysInMonth(), isLeapYear(), isToday(), isTomorrow(), isYesterday(), ordinal(), calendarMonth(). Week numbering and startOf('week') honour the configured weekStartsOn. calendarMonth({ selected, dateFormat }) returns localized weekday labels plus padded date cells for building date pickers and month views. Each cell keeps a stable isoDate plus a locale-aware display formatted value.

Formatting

Two complementary ways to format, both resolved in the configured timezone:

// 1) Intl-powered presets / options (locale-aware)
d.format('short');
d.format({ dateStyle: 'long' });

// 2) Token patterns
d.formatPattern('dddd, MMMM D, YYYY h:mm A');
d.formatPattern('YYYY-MM-DD[T]HH:mm:ssZ');

Presets: short, medium, long, full, dateOnly, timeOnly, weekdayTime, isoStyle12h, isoStyle24h.

Format tokens

Wrap literal text in [brackets]. Names, meridiem and the localized L-family all honour the instance locale — including its field order and separators.

Token Output
YYYY · YY Year (4-digit / 2-digit)
MMMM · MMM · MM · M Month name (full/short) / number (padded/plain)
DD · D · Do Day of month (padded / plain / ordinal — English by default, see ordinal below)
dddd · ddd · dd · d Weekday name (full/short/narrow) / number
HH · H · hh · h Hour, 24h / 12h (padded / plain)
kk · k Hour 1–24 (padded / plain)
mm · m · ss · s · SSS · SS · S Minutes / seconds / fractional seconds (SSS = milliseconds, SS = hundredths, S = tenths)
A · a Meridiem, locale-aware (AM/PM, π.μ./μ.μ., 上午/下午)
Z · ZZ UTC offset +HH:mm / +HHmm
Q · w/ww/wo · W/WW/Wo Quarter · week of year (+ ordinal) · ISO week (+ ordinal)
X · x Unix seconds · Unix milliseconds
L LL LLL LLLL · l ll lll llll · LT LTS Localized date/time — field order & separators follow the locale
datetick('2026-06-25').formatPattern('L');                // '06/25/2026'  (en)
datetick('2026-06-25', { locale: 'en-GB' }).formatPattern('L');  // '25/06/2026'
datetick('2026-06-25', { locale: 'de' }).formatPattern('LL');    // '25. Juni 2026'

Parsing

DateTick.parse(input, pattern, locale?, timezone?) (also datetick.parse) reads the wall-clock values in the given timezone and returns a DateTick. Throws if the input doesn’t match. Parsing mirrors the formatPattern token set, including fractional-second tokens (S/SS/SSS) and UTC-offset tokens (Z/ZZ).

datetick.parse('25/06/2026 22:23', 'DD/MM/YYYY HH:mm', 'en', 'UTC');
datetick.parse('Jun 25, 2026 10:23 PM', 'MMM D, YYYY h:mm A');

// Fractional seconds are scaled by width: .9 = 900ms, .09 = 90ms
datetick.parse('22:23:04.9', 'HH:mm:ss.S').millisecond();  // 900

// A parsed Z/ZZ offset fixes the absolute instant, overriding the timezone argument
datetick.parse('2026-06-25 12:00 +05:30', 'YYYY-MM-DD HH:mm Z', 'en', 'UTC').toISOString();
// '2026-06-25T06:30:00.000Z'

// Localized month names, meridiem and L-family layouts round-trip
datetick.parse('25/06/2026', 'L', 'fr', 'UTC');
datetick.parse('2026-06-25 下午 07:23', 'YYYY-MM-DD A hh:mm', 'zh', 'UTC');

Durations

datetick.duration(2, 'hour').humanize();        // '2 hours'
datetick.duration(-3, 'day').humanize(true);    // '3 days ago'
datetick.duration({ days: 1, hours: 2 }).asMinutes(); // 1560
datetick.duration('P1Y2M10DT2H30M').asDays();     // parse an ISO-8601 duration
datetick.duration(span).toISOString();          // 'P1Y2M3DT4H5M6S'

Create a Duration from a number + unit, a components object, or an ISO-8601 string (e.g. 'PT1H30M', fractional and signed accepted). It’s immutable: as{Milliseconds…Years}() totals, component getters years()…milliseconds(), add/subtract, humanize(withSuffix?), format(pattern?), toISOString()/toJSON(), valueOf(). Fixed ratios: 1 month = 30 days, 1 year = 365 days.

Conversion

toDate(), toArray(), toObject(), toISOString(), toJSON() (so JSON.stringify works), toString(), unix() (seconds), valueOf() (ms — enables +instance and </>).

Timezone conversion keeps the same instant, changing only the zone the wall clock resolves through: utc() and local() (runtime zone) return a new DateTick, while utcOffset() returns the configured zone's offset in minutes (positive east of UTC).

Locale data

localeData() exposes locale metadata resolved through Intl — the Day.js localeData() equivalent, with no locale files to import.

const ld = datetick('2026-06-25', { locale: 'fr' }).localeData();
ld.months();          // ['janvier', 'février', …]
ld.weekdays();        // Sunday-based: ['dimanche', 'lundi', …]
ld.weekdaysShort();   ld.weekdaysMin();
ld.firstDayOfWeek();  // configured weekStartsOn
ld.meridiems();       // { am, pm }
ld.meridiem(15);      // 'PM'  (locale-aware)
ld.ordinal(1);        // '1st'  (or your ordinal override)

Also: months()/monthsShort(). Because everything derives from Intl, coverage matches the runtime’s CLDR data — hundreds of locales, no bundles.

Static helpers

DateTick.guessTimezone(), DateTick.isValid(date), DateTick.isDateTick(value), DateTick.min(...dates), DateTick.max(...dates), DateTick.parse(...), DateTick.duration(...) — all also available on the datetick factory (plus datetick.utc, datetick.unix and the datetick.isDuration(value) type guard, also exported as Duration.isDuration).

App-wide defaults

datetick.withDefaults(options) returns a new, independent factory pre-bound to the given { locale, timezone, weekStartsOn, ordinal }. It mutates nothing global — `datetick` itself is unaffected, so it's safe under SSR and concurrent requests.

const frDateTick = datetick.withDefaults({ locale: 'fr', timezone: 'Europe/Paris' });

frDateTick('2026-06-25').format('long');   // French, Paris
frDateTick.duration(2, 'day').humanize(); // '2 jours'
datetick('2026-06-25').format('long');    // unaffected, still 'en'

Month/weekday names, the L-family layout and meridiem are already localized through Intl. The one thing Intl can’t supply is ordinals, so pass an ordinal function to localize ordinal() and the Do/wo/Wo tokens (no global updateLocale needed):

const el = datetick.withDefaults({ locale: 'el', ordinal: (n) => `${n}η` });
el('2026-06-01').formatPattern('Do');   // '1η'  (meridiem already localized by Intl)

Defaults merge progressively when chained, and a per-call option still wins over the factory's defaults:

const frParisMonday = frDateTick.withDefaults({ weekStartsOn: 1 });
frParisMonday('2026-06-25', { locale: 'de' }).locale(); // 'de' — explicit override wins

"App-wide" just means creating the scoped factory once and importing that instance everywhere instead of the base datetick — nothing global to coordinate:

// src/app-datetick.ts — one shared module, identical in any framework
import datetick from 'datetick';
export const appDateTick = datetick.withDefaults({ locale: 'fr', timezone: 'Europe/Paris' });

// anywhere else
import { appDateTick } from './app-datetick';
appDateTick('2026-06-25').format('long');

If the locale can change at runtime (a language switcher) and components need to react to it, hold the current factory in your framework's own reactivity primitive instead — see the Angular, React and Vue sections below.

Vanilla JS / TypeScript

import datetick from 'datetick';

document.querySelector('#published').textContent = datetick('2024-01-01').format('long');

Via CDN (UMD global datetick):

<script src="https://unpkg.com/datetick/dist/index.umd.min.js"></script>
<script>
  // The UMD global exposes the namespace: datetick.datetick(), datetick.DateTick, datetick.Duration
  console.log(datetick.datetick('2024-01-01').format('medium'));
</script>

Angular

An impure pipe that subscribes to timeAgoLive() and updates itself:

import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import datetick from 'datetick';

@Pipe({ name: 'timeAgo', standalone: true, pure: false })
export class TimeAgoPipe implements PipeTransform, OnDestroy {
  private label = '';
  private lastValue?: Date | string;
  private stop?: () => void;

  constructor(private cdr: ChangeDetectorRef) {}

  transform(value: Date | string): string {
    if (value !== this.lastValue) {
      this.stop?.();
      this.lastValue = value;
      this.stop = datetick(value).timeAgoSubscribe((label) => {
        this.label = label;
        this.cdr.markForCheck();
      });
    }
    return this.label;
  }

  ngOnDestroy(): void {
    this.stop?.();
  }
}
<span>{{ post.createdAt | timeAgo }}</span>

App-wide, switchable locale — a service exposing a signal<DateTickFactory>:

import { Injectable, Signal, signal } from '@angular/core';
import datetick, { DateTickFactory } from 'datetick';

@Injectable({ providedIn: 'root' })
export class AppDateTickService {
  private readonly _datetick = signal<DateTickFactory>(datetick.withDefaults({ locale: 'en' }));
  readonly datetick: Signal<DateTickFactory> = this._datetick.asReadonly();

  setLocale(locale: string): void {
    this._datetick.set(datetick.withDefaults({ locale }));
  }
}
import { Component, Input } from '@angular/core';

@Component({ selector: 'app-post', template: `<time>{{ appDateTick.datetick()(createdAt).format('long') }}</time>` })
export class PostComponent {
  @Input({ required: true }) createdAt!: string;

  constructor(readonly appDateTick: AppDateTickService) {}
}

React

A hook that subscribes to timeAgoLive():

import { useEffect, useState } from 'react';
import datetick from 'datetick';

export const TimeAgo = ({ date }: { date: string | Date }) => {
  const [label, setLabel] = useState('');
  useEffect(() => {
    return datetick(date).timeAgoSubscribe(setLabel);
  }, [date]);
  return <time>{label}</time>;
};

App-wide, switchable locale — a Context provider + hook:

import { createContext, useContext, useMemo, type ReactNode } from 'react';
import datetick, { DateTickFactory } from 'datetick';

const DateTickContext = createContext<DateTickFactory>(datetick);

export const DateTickProvider = ({ locale, children }: { locale: string; children: ReactNode }) => {
  const value = useMemo(() => datetick.withDefaults({ locale }), [locale]);
  return <DateTickContext.Provider value={value}>{children}</DateTickContext.Provider>;
};

export const useAppDateTick = (): DateTickFactory => useContext(DateTickContext);
// Switching language: render <DateTickProvider locale={locale}> with new state from useState/useReducer
const PublishedDate = ({ date }: { date: string }) => {
  const appDateTick = useAppDateTick();
  return <time>{appDateTick(date).format('long')}</time>;
};

Vue 3

A composable that subscribes to timeAgoLive():

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import datetick from 'datetick';

const props = defineProps<{ date: string | Date }>();
const label = ref('');
let stop: (() => void) | undefined;

onMounted(() => {
  stop = datetick(props.date).timeAgoSubscribe((v) => (label.value = v));
});
onUnmounted(() => stop?.());
</script>

<template>
  <time>{{ label }}</time>
</template>

App-wide, switchable locale — provide/inject + a composable:

// app-datetick.ts
import { inject, ref, type App, type Ref } from 'vue';
import datetick, { DateTickFactory } from 'datetick';

const DATETICK_KEY = Symbol('appDateTick');

export const provideAppDateTick = (app: App, initialLocale: string): Ref<DateTickFactory> => {
  const current = ref(datetick.withDefaults({ locale: initialLocale })) as Ref<DateTickFactory>;
  app.provide(DATETICK_KEY, current);
  return current; // current.value = datetick.withDefaults({ locale: next }) to switch language
};

export const useAppDateTick = (): Ref<DateTickFactory> => inject(DATETICK_KEY) as Ref<DateTickFactory>;
<script setup lang="ts">
import { useAppDateTick } from './app-datetick';
const { date } = defineProps<{ date: string }>();
const appDateTick = useAppDateTick();
</script>

<template>
  <time>{{ appDateTick(date).format('long') }}</time>
</template>

Live playground

Everything below runs the real library in your browser. Pick a date, locale, timezone and week start.

Inputs
Formatting
formatPattern
Components & calendar info
Manipulation (immutable)
Comparison & live relative time
timeAgoLive() — updates live
Subscribed to the live handle; re-emits on an adaptive schedule.
Durations
duration
The exact calls behind these values