Note: svelte-time@2 requires Svelte 5; its internals use runes. The consuming app does not need runes mode enabled, because runes are opt-in per component.
Use svelte-time@1.0.0 for Svelte 3, 4, and 5 (non-Runes mode).
svelte-time formats timestamps and durations as Svelte components and actions, and puts the machine-readable value on a semantic time element.
It uses day.js.
<!-- Input -->
<Time relative />
<!-- Output rendered in the DOM -->
<time title="May 15, 2022" datetime="2022-05-15T18:03:57.430Z">
a few seconds ago
</time>Try it in the Svelte REPL.
Three primitives share the same timer logic:
| Primitive | Export | Use it when... |
|---|---|---|
| Component | Time |
you want a declarative element with a timestamp prop and full SSR support |
| Action | svelteTime |
you want use:svelteTime on a plain element, no extra markup, and don't need the shared adaptive timer |
| Attachment | time |
you want @attach with reactive options and the same shared adaptive timer as the Time component |
The dayjs re-export is a convenience utility, not a rendering primitive.
| Package version | Svelte version | Notes |
|---|---|---|
| 1.x | 3, 4, 5 (non-runes) | Uses export let and legacy reactivity |
| 2.x | 5+ (component, action); 5.29+ (time attachment) |
Internals use runes; the consuming app does not need runes mode enabled |
# npm
npm i svelte-time
# pnpm
pnpm i svelte-time
# Bun
bun i svelte-time
# Yarn
yarn add svelte-timeThe displayed time defaults to new Date().toISOString() and is formatted as "MMM DD, YYYY".
<script>
import Time from "svelte-time";
</script>
<Time />The timestamp prop can be any of the following dayjs values: string | number | Date | Dayjs. String timestamps are written to datetime as-is; Date/Dayjs/number inputs are normalized to ISO 8601; invalid inputs omit the attribute.
<Time timestamp="2020-02-01" />
<Time timestamp={new Date()} />
<Time timestamp={1e10} />Use the format prop to format the timestamp. Refer to the dayjs format documentation for acceptable formats.
<Time timestamp="2020-02-01" format="dddd @ h:mm A · MMMM D, YYYY" />
<Time timestamp={new Date()} format="YYYY/MM/DD" />
<Time timestamp={1e10} format="ddd" />Set relative to true to show time relative to now, like "4 days ago".
<Time relative />
<Time relative timestamp="2021-02-02" />
<Time relative timestamp={1e10} />Use the format prop to customize the format.
<Time relative format="dddd @ h:mm A · MMMM D, YYYY" />With relative, the formatted timestamp goes on the title attribute. Pass your own title to override it.
<Time relative title="Custom title" />Pass title={undefined} to omit the attribute.
<Time relative title={undefined} />Pass a children snippet to render custom markup. The snippet gets the formatted value; the component still owns <time>, title, and datetime.
<Time relative timestamp={post.createdAt}>
{#snippet children(formatted)}
<strong>{formatted}</strong>
{/snippet}
</Time>Set live to true for a live relative timestamp. Updates follow an adaptive schedule based on age. See Performance for the schedule and how the timer works.
<Time live relative />Pass a number to live for a fixed interval in milliseconds.
<!-- Update every 30 seconds -->
<Time live={30 * 1_000} relative />
<!-- Update every 10 minutes -->
<Time live={10 * 60 * 1_000} relative />Set relativeThreshold to an age in ms to switch from relative to absolute format once the timestamp is old enough. Only applies when relative is true. With live, the switch happens as time passes; without live, it only affects the value at render time.
<!-- Shows "a minute ago", flips to "4:40 am" after 2 hours -->
<Time relative live format="h:mm a" relativeThreshold={2 * 60 * 60 * 1000} />Built to keep lots of live timestamps cheap.
- One timer, not n. All
Timecomponents andtimeattachments on the same live interval share one clock. A feed with 1,000 timestamps on the same tier runs onesetInterval, not 1,000. Timers start with the first live consumer and stop when the last unmounts. An idle page runs none. - Adaptive refresh.
live={true}ticks by age: every 10s under a minute, 30s under an hour, 5 minutes under a day, then hourly. Fresh stamps stay within about 10 seconds of truth; day-old ones update 60× less than fixed 60-second polling. Pass a number tolivefor a fixed interval. - Background tabs. Browsers throttle hidden-tab timers. The shared clock refreshes as soon as the tab is visible again.
- Cheap updates. Each component parses its timestamp once per tick. The same
dayjsinstance feeds the text and thetitle. ThesvelteTimeaction andtimeattachment write viatextContent, so the tick path avoids layout-forcing DOM APIs.
The svelteTime action is the exception: each node gets its own setInterval (60 seconds by default, or a custom ms interval), with no shared timer and no adaptive schedule. Prefer Time or the time attachment when many timestamps are live.
The Time component renders fully on the server: text, title, and datetime are in the HTML, and no timers start during SSR, even with live.
The svelteTime action and time attachment render an empty <time> until hydration, because neither runs on the server. Prefer Time when SSR content matters for SEO, no-JS, or avoiding a flash.
Pass an explicit timestamp under SSR. The default new Date().toISOString() is re-evaluated on the client during hydration, so server and client values can disagree, and relative text can cross a threshold between render and hydration, like "a few seconds ago" becoming "a minute ago".
Use the svelteTime action to format a timestamp on a raw HTML element.
The API is the same as the Time component.
<script>
import { svelteTime } from "svelte-time";
</script>
<time use:svelteTime></time>
<time
use:svelteTime={{
timestamp: "2021-02-02",
format: "dddd @ h:mm A · MMMM D, YYYY",
}}
></time>Set relative to true to use relative time.
<time
use:svelteTime={{
relative: true,
timestamp: "2021-02-02",
}}
></time>
<time
use:svelteTime={{
relative: true,
timestamp: "2021-02-02",
format: "dddd @ h:mm A · MMMM D, YYYY",
}}
></time>To customize or omit the title attribute, use the title prop.
<time
use:svelteTime={{
relative: true,
title: "Custom title",
timestamp: "2021-02-02",
}}
></time>
<time
use:svelteTime={{
relative: true,
title: undefined,
timestamp: "2021-02-02",
}}
></time>Like the Time component, live only works with relative time.
<time
use:svelteTime={{
relative: true,
live: true,
}}
></time>Specify a custom update interval using the live prop.
<time
use:svelteTime={{
relative: true,
live: 30 * 1_000, // Update every 30 seconds
}}
></time>Use relativeThreshold to switch to the absolute format once the timestamp's age (ms) meets or exceeds it.
<time
use:svelteTime={{
relative: true,
live: true,
format: "h:mm a",
relativeThreshold: 2 * 60 * 60 * 1_000, // 2 hours
}}
></time>Attachments (@attach, Svelte 5.29+) succeed actions. The time attachment is like svelteTime, but options stay reactive: it re-runs when any reactive value used to build options changes, including inline template objects. In live mode it shares the Time component's global timer instead of a per-element setInterval.
<script>
import { time } from "svelte-time";
</script>
<time {@attach time({ timestamp: "2021-02-02", format: "YYYY-MM-DD" })}></time>Because options are reactive, an inline object built from $state updates the element automatically. No update() contract:
<script lang="ts">
import { time } from "svelte-time";
let timestamp = $state("2021-02-02");
</script>
<time {@attach time({ timestamp, format: "YYYY-MM-DD" })}></time>
<button onclick={() => (timestamp = "2021-02-03")}>Update</button>@attach needs Svelte 5.29+. The svelteTime action still works.
relativeThreshold works the same as on Time and svelteTime:
<time
{@attach time({
relative: true,
live: true,
format: "h:mm a",
relativeThreshold: 2 * 60 * 60 * 1_000, // 2 hours
})}
></time>Set withoutSuffix to true to drop the "ago" suffix from relative time.
<script>
import Time, { dayjs } from "svelte-time";
const pastDate = dayjs().subtract(2, "days").toISOString();
const futureDate = dayjs().add(2, "days").toISOString();
</script>
<!-- Past date -->
<Time relative timestamp={pastDate} />
<!-- Output: "2 days ago" -->
<Time relative timestamp={pastDate} withoutSuffix />
<!-- Output: "2 days" -->
<!-- Future date -->
<Time relative timestamp={futureDate} />
<!-- Output: "in 2 days" -->
<Time relative timestamp={futureDate} withoutSuffix />
<!-- Output: "2 days" -->Same options work on the svelteTime action:
<time
use:svelteTime={{
relative: true,
timestamp: "2021-02-02",
withoutSuffix: true,
}}
></time>Set relativeStyle to "micro" for compact relative time like "4d" instead of "4 days ago". Useful in dense UIs such as comment lists. Only applies when relative is true. Output always uses English unit letters (y/mo/d/h/m/s), even if locale is set, because dayjs's relativeTime locale tables have no single-letter forms.
<Time relative timestamp={pastDate} />
<!-- Output: "4 days ago" -->
<Time relative relativeStyle="micro" timestamp={pastDate} />
<!-- Output: "4d" instead of "4 days ago" -->Same options work on the svelteTime action and the time attachment:
<time
use:svelteTime={{
relative: true,
timestamp: pastDate,
relativeStyle: "micro",
}}
></time>
<time
{@attach time({
relative: true,
timestamp: pastDate,
relativeStyle: "micro",
})}
></time>This package re-exports dayjs.
Note: the exported dayjs already has the relativeTime plugin and the duration plugin applied.
<script>
import { dayjs } from "svelte-time";
let timestamp = $state("");
</script>
<button onclick={() => (timestamp = dayjs().format("HH:mm:ss.SSSSSS"))}>
Update {timestamp}
</button>The default dayjs locale is English. Nothing else is loaded unless you import it: pull each locale from dayjs once, then pass its key. See supported locales.
Import the relevant language from dayjs and use the locale prop.
<script>
import "dayjs/locale/de"; // German
import "dayjs/locale/es"; // Spanish
import "dayjs/locale/fr"; // French
import "dayjs/locale/ja"; // Japanese
import Time from "svelte-time";
</script>
<Time timestamp="2024-01-01" format="dddd, MMMM D, YYYY" locale="de" />
<Time timestamp="2024-01-01" format="dddd, D [de] MMMM [de] YYYY" locale="es" />
<Time timestamp="2024-01-01" format="dddd D MMMM YYYY" locale="fr" />
<Time timestamp="2024-01-01" format="YYYY年M月D日(dddd)" locale="ja" />TypeScript also exports Locales, TimeProps, SvelteTimeOptions, and RelativeStyle for wrappers and action options.
import type {
Locales,
TimeProps,
SvelteTimeOptions,
RelativeStyle,
} from "svelte-time";
const exampleLocale: Locales = "de";
let locale = $state<Locales>("de");
let style: RelativeStyle = $state("default");Pass locale to svelteTime to format in another language.
<script>
import "dayjs/locale/de"; // German locale
import "dayjs/locale/es"; // Spanish locale
import { svelteTime } from "svelte-time";
</script>
<time
use:svelteTime={{
timestamp: "2024-01-01",
format: "dddd, MMMM D, YYYY",
locale: "de",
}}
></time>
<time
use:svelteTime={{
relative: true,
timestamp: "2024-01-01",
locale: "es",
}}
></time>locale also applies to relative time.
<script>
import "dayjs/locale/de"; // German
import "dayjs/locale/es"; // Spanish
import "dayjs/locale/fr"; // French
import "dayjs/locale/ja"; // Japanese
import Time from "svelte-time";
</script>
<Time relative timestamp="2024-01-01" locale="de" />
<Time relative timestamp="2024-01-01" locale="es" />
<Time relative timestamp="2024-01-01" locale="fr" />
<Time relative timestamp="2024-01-01" locale="ja" />withoutSuffix works with locales too:
<script>
import "dayjs/locale/de"; // German
import "dayjs/locale/es"; // Spanish
import "dayjs/locale/fr"; // French
import Time from "svelte-time";
</script>
<Time relative timestamp="2024-01-01" locale="de" withoutSuffix />
<!-- Output: "2 Jahre" (German, without "vor") -->
<Time relative timestamp="2024-01-01" locale="es" withoutSuffix />
<!-- Output: "2 años" (Spanish, without "hace") -->
<Time relative timestamp="2024-01-01" locale="fr" withoutSuffix />
<!-- Output: "2 ans" (French, without "il y a") -->locale is reactive. Bind it to $state and every <Time> updates when it changes.
<script lang="ts">
import "dayjs/locale/de"; // German
import "dayjs/locale/es"; // Spanish
import "dayjs/locale/fr"; // French
import Time, { type Locales } from "svelte-time";
let locale = $state<Locales>("en");
</script>
<button onclick={() => (locale = "en")}>English</button>
<button onclick={() => (locale = "de")}>Deutsch</button>
<button onclick={() => (locale = "es")}>Español</button>
<button onclick={() => (locale = "fr")}>Français</button>
<Time timestamp="2024-01-01" format="dddd, MMMM D, YYYY" {locale} />
<Time relative timestamp="2024-01-01" {locale} />Or set a default with dayjs.locale, or pass a dayjs instance that already has a locale.
<script>
import "dayjs/locale/de"; // German
import Time, { dayjs } from "svelte-time";
</script>
<Time timestamp={dayjs().locale("de")} format="dddd, MMMM D, YYYY" />To set a global default locale:
<script>
import "dayjs/locale/de"; // German locale
import { dayjs } from "svelte-time";
// Set the default locale to German.
dayjs.locale("de");
</script>Pass tz with an IANA timezone to render in that zone without building dayjs.tz(...) yourself. You must extend dayjs's utc and timezone plugins first; if they're missing, tz throws instead of failing silently.
<script>
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import Time, { dayjs } from "svelte-time";
dayjs.extend(utc);
dayjs.extend(timezone);
</script>
<Time
timestamp="2013-11-18T11:55:20Z"
tz="America/Toronto"
format="YYYY-MM-DDTHH:mm:ss"
/>To use a custom timezone, import the utc and timezone plugins from dayjs.
<script>
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import Time, { dayjs } from "svelte-time";
dayjs.extend(utc);
dayjs.extend(timezone);
</script>
<Time
timestamp={dayjs("2013-11-18 11:55:20").tz("America/Toronto")}
format="YYYY-MM-DDTHH:mm:ss"
/>Use the dayjs.tz.setDefault method to set a custom timezone as the default.
<script>
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import Time, { dayjs } from "svelte-time";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault("America/New_York");
</script>Note:
dayjs.tz.setDefault(...)only affects values built withdayjs.tz(...). It does not change what<Time>renders on its own. Use thetzprop for the common case, or pass adayjs.tz(value)result astimestampif you need the global default.
Use the dayjs.tz.guess method to guess the user's timezone.
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.guess(); // America/New_YorkTo retrieve the abbreviated time zone, extend the advancedFormat plugin.
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
+ import advancedFormat from "dayjs/plugin/advancedFormat";
import { dayjs } from "svelte-time";
dayjs.extend(utc);
dayjs.extend(timezone);
+ dayjs.extend(advancedFormat);Then, use the dayjs().local method to get the user's local time zone and format it using the "z" advanced option.
dayjs().local().format("z"); // EST
dayjs().local().format("zzz"); // Eastern Standard TimeDuration formats a span of time, like a video length. Time formats a point in time. For pause/resume count-up, use Stopwatch. For a countdown to a future instant, use Countdown. For a span between two fixed endpoints, like an event start/end, use TimeRange. There is also a svelteDuration action and a duration attachment.
value accepts a number with unit, an ISO 8601 duration string like "PT1H30M", a plain object of unit fields, or a dayjs Duration instance. Default format is "HH:mm:ss".
<script>
import { Duration } from "svelte-time";
</script>
<!-- Default format: "HH:mm:ss" -->
<Duration value={3661000} />
<!-- Output: "01:01:01" -->
<!-- unit converts a plain number before formatting -->
<Duration value={90} unit="seconds" format="mm:ss" />
<!-- Output: "01:30" -->dayjs's own duration.format() drops magnitude above the units in the template. Ours rolls that magnitude into the largest unit that is present, which is useful for players that hide hours until needed:
<Duration value={5400000} format="mm:ss" />
<!-- Output: "90:00", not "30:00" -->Set humanize to true for a natural-language string via dayjs's duration.humanize(), instead of format. Set withSuffix to true for a relative suffix like "in an hour" or "an hour ago". Suffixes are off by default; a video length isn't relative to now.
<script>
import { Duration } from "svelte-time";
</script>
<Duration value={3600000} humanize />
<!-- Output: "an hour" -->
<Duration value={3600000} humanize withSuffix />
<!-- Output: "in an hour" -->Use locale for other languages. Import the locale from dayjs first.
<script>
import "dayjs/locale/de";
import { Duration } from "svelte-time";
</script>
<Duration value={3600000} humanize locale="de" />
<!-- Output: "eine Stunde" -->Pass since instead of value to show elapsed time since that instant. With live, Duration ticks on the same adaptive shared clock as Time's relative live mode. value and unit are ignored when since is set.
<script>
import { Duration } from "svelte-time";
const since = new Date().toISOString();
</script>
<Duration {since} live format="HH:mm:ss" />Pass a number to live for a fixed interval:
<Duration since={startedAt} live={1000} format="HH:mm:ss" />This mode is a passive now - since readout. Pausing means recomputing since on every resume so the paused gap is excluded. For built-in pause/resume, use Stopwatch.
The svelteDuration action formats a duration on a raw HTML element. Same API as the Duration component.
<script>
import { svelteDuration } from "svelte-time";
</script>
<time use:svelteDuration={{ value: 3661000 }}></time>The duration attachment is the attachment form of svelteDuration, with the same reactive options as time.
<script>
import { duration } from "svelte-time";
</script>
<time {@attach duration({ value: 3661000 })}></time>Stopwatch counts up from since, with a running prop for pause/resume. Unlike Duration's since/live mode, it owns the pause bookkeeping: pause freezes the display, resume excludes the paused gap. since defaults to mount time; changing it later resets to zero from the new anchor.
<script>
import { Stopwatch } from "svelte-time";
let since = $state(new Date());
</script>
<!-- Ticks live by default, once per second -->
<Stopwatch {since} />
<!-- Changing `since` restarts the stopwatch -->
<button onclick={() => (since = new Date())}>Restart</button>Pause and resume with running. The children snippet gets the formatted value and the current running state:
<script>
import { Stopwatch } from "svelte-time";
const since = new Date();
let running = $state(true);
</script>
<Stopwatch {since} {running}>
{#snippet children(formatted, isRunning)}
{formatted}
{isRunning ? "(running)" : "(paused)"}
{/snippet}
</Stopwatch>
<button onclick={() => (running = !running)}>
{running ? "Pause" : "Resume"}
</button>The svelteStopwatch action counts up on a raw HTML element. Same API as Stopwatch.
<script>
import { svelteStopwatch } from "svelte-time";
let running = $state(true);
</script>
<time use:svelteStopwatch={{ since: new Date(), running }}></time>The stopwatch attachment is the attachment form of svelteStopwatch, with the same reactive options as time, duration, and countdown.
<script>
import { stopwatch } from "svelte-time";
let running = $state(true);
</script>
<time {@attach stopwatch({ since: new Date(), running })}></time>Countdown counts down to a future to timestamp: a Date, ISO string, or anything dayjs accepts. The display is to - now, clamped at zero. Unlike Duration, live defaults to true and ticks every second, because the last seconds of a countdown matter more than the adaptive "x minutes ago" schedule. Changing to restarts it.
<script>
import { Countdown } from "svelte-time";
let to = $state(new Date(Date.now() + 20_000));
</script>
<!-- Ticks live by default, once per second -->
<Countdown {to} />
<!-- format hides hours until they're needed -->
<Countdown {to} format="mm:ss" />
<!-- Changing `to` restarts the countdown -->
<button onclick={() => (to = new Date(Date.now() + 20_000))}>Reset</button>oncomplete fires once when the countdown reaches to. If to is already past, it fires immediately. If you later change to to another already-elapsed instant, it fires again. The children snippet gets a done flag with the formatted value, so you can swap markup when it finishes without a separate $effect.
<script>
import { Countdown } from "svelte-time";
let to = $state(new Date(Date.now() + 5000));
</script>
<Countdown {to} oncomplete={() => console.log("done!")}>
{#snippet children(formatted, done)}
{done ? "Done!" : formatted}
{/snippet}
</Countdown>The svelteCountdown action counts down on a raw HTML element. Same API as Countdown.
<script>
import { svelteCountdown } from "svelte-time";
const to = new Date(Date.now() + 20_000);
</script>
<time use:svelteCountdown={{ to, oncomplete: () => console.log("done!") }}></time>The countdown attachment is the attachment form of svelteCountdown, with the same reactive options as time and duration.
<script>
import { countdown } from "svelte-time";
const to = new Date(Date.now() + 20_000);
</script>
<time {@attach countdown({ to, oncomplete: () => console.log("done!") })}></time>TimeRange formats a span between two fixed instants, like an event start/end. Duration endpoints are open-ended. HTML datetime holds one value, so a range needs two: TimeRange renders two <time> elements, each with its own datetime, joined by a separator.
format uses dayjs .format() tokens, same default as Time, and applies independently to start and end. v1 does not dedupe a shared year or month. See API for the full props list.
<script>
import { TimeRange } from "svelte-time";
</script>
<!-- An event's start/end -->
<TimeRange
start="2024-06-05"
end="2024-06-10"
/>
<!-- Output: "Jun 05, 2024 – Jun 10, 2024" -->For a humanized span between endpoints like "3 days", compose Duration:
<Duration value={dayjs(end).diff(start)} humanize />format isn't limited to dates. Pass a time-only format, and optionally a custom separator, for a same-day window.
<script>
import { TimeRange } from "svelte-time";
</script>
<!-- A same-day meeting window -->
<TimeRange
start="2024-06-05T08:00:00"
end="2024-06-05T10:00:00"
format="h:mm A"
/>
<!-- Output: "8:00 AM – 10:00 AM" -->Use locale to format both endpoints. Import the locale from dayjs first.
<script>
import "dayjs/locale/de";
import { TimeRange } from "svelte-time";
</script>
<TimeRange
start="2024-06-05"
end="2024-06-10"
format="D. MMMM"
locale="de"
/>
<!-- Output: "5. Juni – 10. Juni" -->Pass a children snippet to replace the default output entirely. On Time/Duration, children only swaps inner text. On TimeRange, the snippet owns both <time> elements and the separator, because format alone can't condense a range (shared date once, times on each side). Format each side with dayjs inside the snippet.
<script>
import { dayjs, TimeRange } from "svelte-time";
const start = "2024-06-05T08:00:00";
const end = "2024-06-05T10:00:00";
</script>
<!-- Condensed: date once, time on each side -->
<TimeRange {start} {end}>
{#snippet children({ startDatetime, endDatetime })}
<time datetime={startDatetime}>{dayjs(start).format("MMM D, h:mm A")}</time>
–
<time datetime={endDatetime}>{dayjs(end).format("h:mm A")}</time>
{/snippet}
</TimeRange>
<!-- Output: "Jun 5, 8:00 AM – 10:00 AM" -->The svelteTimeRange action attaches to a wrapper element and fills it with two <time> children and the separator. An action can only bind one node, and a range needs two.
<script>
import { svelteTimeRange } from "svelte-time";
</script>
<span use:svelteTimeRange={{ start: "2024-06-05", end: "2024-06-10" }}></span>The timeRange attachment is the attachment form of svelteTimeRange, same reactive options, same wrapper usage.
<script>
import { timeRange } from "svelte-time";
</script>
<span {@attach timeRange({ start: "2024-06-05", end: "2024-06-10" })}></span>The formatting logic and shared clock behind <Time> and svelteTime are also available as standalone helpers, for an aria-label, document.title, a toast, or server code.
<script>
import { now, relativeTime } from "svelte-time";
// Re-derives every 30s from the shared timer. No timer of its own.
const label = $derived(relativeTime(post.createdAt, { from: now(30_000) }));
</script>
<button aria-label="Posted {label}">…</button>Reactive current time from the shared ticker. Read inside an effect or derived and the caller re-runs every intervalMs (default 60_000). All readers of the same interval share one timer. On the server, returns a fresh non-reactive value.
Formats a timestamp as a string with the same output as <Time>'s format prop: same defaults, same locale fallback.
Formats a relative time string with the same output as <Time relative />. Pass from for the reference point; use now(...) to keep it live.
A zero-dependency alternative on the runtime's native Intl APIs. No dayjs, no locale files, no plugins. It ships next to the dayjs-based primitives above; it does not replace them.
Trade-off: formatting only, not a dayjs stand-in. It will not parse arbitrary date strings, will not do arithmetic (
add/diff/startOf, etc.), and has no token format strings like"MMM DD, YYYY". You hand it aDateor timestamp you already have, and it formats withIntl.DateTimeFormatOptionsordateStyle/timeStylepresets. It also usesIntl.DurationFormat, the newest of the threeIntlAPIs here (Baseline since March 2025, less proven thanDateTimeFormat/RelativeTimeFormat, which have been widely available since 2020). See the full comparison before picking this over the dayjs package.
Intl.DateTimeFormat, Intl.RelativeTimeFormat, and Intl.DurationFormat are Baseline in modern browsers, Node, Bun, and Deno, so this subpackage adds no bundle bytes beyond the platform. Import from svelte-time/intl instead of svelte-time.
# Same install as above. svelte-time/intl is a subpath of the same package
npm i svelte-timeSame component / action / attachment shape as the main package for Time, TimeRange, Stopwatch, and Countdown. No intl Duration component; formatDuration below already covers fixed spans.
| Primitive | Export | Notes |
|---|---|---|
| Component | Time |
same shape as the main Time component |
| Component | TimeRange |
renders a single condensed range via formatRange (see below) |
| Component | Stopwatch |
same shape as the main Stopwatch component |
| Component | Countdown |
same shape as the main Countdown component |
| Action | svelteTime |
same shape as the main svelteTime action |
| Action | svelteTimeRange |
same shape as the main svelteTimeRange action |
| Action | svelteStopwatch |
same shape as the main svelteStopwatch action |
| Action | svelteCountdown |
same shape as the main svelteCountdown action |
| Attachment | time |
same shape as the main time attachment |
| Attachment | timeRange |
attachment version of TimeRange |
| Attachment | stopwatch |
attachment version of Stopwatch |
| Attachment | countdown |
attachment version of Countdown |
options is an Intl.DateTimeFormatOptions object, not a dayjs token string. Default is dateStyle: "medium".
<script>
import { Time } from "svelte-time/intl";
</script>
<Time /><Time
timestamp="2020-02-01"
options={{ weekday: "long", year: "numeric", month: "short", day: "numeric" }}
/>
<Time timestamp={new Date()} options={{ year: "numeric", month: "2-digit", day: "2-digit" }} />Intl.DateTimeFormat style presets ("short", "medium", "long", "full") are a locale-aware alternative to writing a token string by hand.
<Time {timestamp} options={{ dateStyle: "short" }} />
<Time {timestamp} options={{ dateStyle: "full" }} />
<Time {timestamp} options={{ dateStyle: "medium", timeStyle: "short" }} />Set relative to true, same as the main package. numeric maps to Intl.RelativeTimeFormat: "auto" (default) prefers words like "yesterday" when the locale has them; "always" forces "1 day ago".
<Time relative timestamp={Date.now() - 4 * 86_400_000} />
<Time relative timestamp={Date.now() + 2 * 3_600_000} /><!-- numeric="auto" (default): "yesterday" -->
<Time relative {timestamp} />
<!-- numeric="always": "1 day ago" -->
<Time relative {timestamp} numeric="always" />Same live prop as the main Time component: true for the adaptive shared-clock schedule, or a number for a fixed interval in ms.
<Time relative live {timestamp} />Pass any BCP-47 locale tag directly. No separate locale files, unlike dayjs. The runtime's ICU data covers what it supports.
<Time {timestamp} locale="de" options={{ dateStyle: "long" }} />
<Time {timestamp} locale="ja" options={{ dateStyle: "long" }} />
<Time {timestamp} locale="ar" options={{ dateStyle: "long" }} />
<Time relative {timestamp} locale="fr" />calendar and numberingSystem work natively, with no plugins. dayjs needs extra code for that.
<Time {timestamp} options={{ dateStyle: "long", calendar: "japanese" }} />
<Time {timestamp} options={{ dateStyle: "long", calendar: "islamic" }} />
<Time {timestamp} options={{ dateStyle: "long", calendar: "buddhist" }} />
<Time {timestamp} options={{ dateStyle: "long", numberingSystem: "arab" }} />Formats a span via Intl.DateTimeFormat.prototype.formatRange, which condenses shared parts, e.g. "Jan 10 – 15, 2026". The main package's TimeRange renders two <time> elements because one datetime can't hold a range. This renders one <time> with the native condensed text and an ISO 8601 interval (start/end) as datetime.
<script>
import { TimeRange } from "svelte-time/intl";
</script>
<!-- Output: "Jan 10 – 15, 2026" -->
<TimeRange start="2026-01-10" end="2026-01-15" />
<!-- Spans months/years automatically -->
<TimeRange start="2026-01-10" end="2026-03-02" />
<TimeRange start="2026-01-10" end="2026-01-15" options={{ dateStyle: "long" }} />Same shape as the main package svelteTime action.
<script>
import { svelteTime } from "svelte-time/intl";
</script>
<time use:svelteTime></time>
<time
use:svelteTime={{
timestamp: "2021-02-02",
options: { dateStyle: "long" },
}}
></time><script>
import { time } from "svelte-time/intl";
</script>
<time {@attach time({ timestamp: "2021-02-02", options: { dateStyle: "long" } })}></time><script>
import { svelteTimeRange } from "svelte-time/intl";
</script>
<time use:svelteTimeRange={{ start: "2026-01-10", end: "2026-01-15" }}></time><script>
import { timeRange } from "svelte-time/intl";
</script>
<time {@attach timeRange({ start: "2026-01-10", end: "2026-01-15" })}></time>Same shape as the main Stopwatch: counts up from since, with running for pause/resume. style replaces format/humanize/withSuffix. "digital" (default) is "HH:mm:ss"-style; "long"/"short"/"narrow" use Intl.DurationFormat.
<script>
import { Stopwatch } from "svelte-time/intl";
let since = $state(new Date());
</script>
<!-- Ticks live by default, once per second -->
<Stopwatch {since} />
<!-- Changing `since` restarts the stopwatch -->
<button onclick={() => (since = new Date())}>Restart</button>Pause and resume with running. The children snippet gets the formatted value and the current running state:
<script>
import { Stopwatch } from "svelte-time/intl";
const since = new Date();
let running = $state(true);
</script>
<Stopwatch {since} {running}>
{#snippet children(formatted, isRunning)}
{formatted}
{isRunning ? "(running)" : "(paused)"}
{/snippet}
</Stopwatch>
<button onclick={() => (running = !running)}>
{running ? "Pause" : "Resume"}
</button><script>
import { svelteStopwatch } from "svelte-time/intl";
let running = $state(true);
</script>
<time use:svelteStopwatch={{ since: new Date(), running }}></time><script>
import { stopwatch } from "svelte-time/intl";
let running = $state(true);
</script>
<time {@attach stopwatch({ since: new Date(), running })}></time>Same shape as the main Countdown: counts down to a future to, clamped at zero, ticking every second by default.
<script>
import { Countdown } from "svelte-time/intl";
let to = $state(new Date(Date.now() + 20_000));
</script>
<!-- Ticks live by default, once per second -->
<Countdown {to} />
<!-- style="long" gives a humanized readout -->
<Countdown {to} style="long" />
<!-- Changing `to` restarts the countdown -->
<button onclick={() => (to = new Date(Date.now() + 20_000))}>Reset</button>oncomplete fires once when the countdown reaches to. The children snippet gets a done flag with the formatted value.
<script>
import { Countdown } from "svelte-time/intl";
let to = $state(new Date(Date.now() + 5000));
</script>
<Countdown {to} oncomplete={() => console.log("done!")}>
{#snippet children(formatted, done)}
{done ? "Done!" : formatted}
{/snippet}
</Countdown><script>
import { svelteCountdown } from "svelte-time/intl";
</script>
<time use:svelteCountdown={{ to: new Date(Date.now() + 20_000), oncomplete: () => console.log("done!") }}></time><script>
import { countdown } from "svelte-time/intl";
const to = new Date(Date.now() + 20_000);
</script>
<time {@attach countdown({ to, oncomplete: () => console.log("done!") })}></time>Uses Intl.DurationFormat. style accepts "digital" (default, e.g. "1:30:25"), "long", "short", or "narrow".
<script>
import { formatDuration } from "svelte-time/intl";
const ms = 5_425_000; // 1h 30m 25s
</script>
<p>digital (default): {formatDuration(ms)}</p>
<p>long: {formatDuration(ms, { style: "long" })}</p>
<p>narrow: {formatDuration(ms, { style: "narrow" })}</p>
<p>negative: {formatDuration(-42_000)}</p>formatTime, relativeTime, and formatRange are also available standalone, outside a <time> element.
<script>
import { formatRange, formatTime, relativeTime } from "svelte-time/intl";
const timestamp = "2026-01-10T00:00:00Z";
</script>
<ul>
<li>formatTime: {formatTime(timestamp)}</li>
<li>relativeTime: {relativeTime(timestamp)}</li>
<li>formatRange: {formatRange(timestamp, "2026-01-15T00:00:00Z")}</li>
</ul>svelte-time/intl drops dayjs parsing, arithmetic, and format strings for a zero-dependency footprint. If the app already uses dayjs, or needs any of the "no" rows below, stick with the dayjs package.
| Aspect | svelte-time (dayjs) |
svelte-time/intl |
|---|---|---|
| Runtime dependency | dayjs + relativeTime/duration plugins |
none, built on platform Intl APIs |
| Format style | token strings, e.g. "MMM DD, YYYY" |
Intl.DateTimeFormatOptions fields, or dateStyle/timeStyle presets |
| Parsing arbitrary formats | yes, via dayjs | no, formats a Date/timestamp you already have |
| Date arithmetic | yes (add/diff/startOf, etc.) |
no |
| Date ranges | yes, TimeRange renders two <time> elements + separator |
yes, TimeRange/formatRange render one condensed native string |
| Alternate calendars/numbering systems | requires extra dayjs plugins | built in (calendar/numberingSystem options) |
| Locale loading | import each locale from dayjs/locale/* |
none, covered by the runtime's built-in ICU data |
| Browser baseline support | n/a (bundled library) | DateTimeFormat/RelativeTimeFormat/formatRange: widely available since 2020; DurationFormat: newer (Baseline since March 2025) |
| Name | Type | Default value | Description |
|---|---|---|---|
| timestamp | string | number | Date | Dayjs |
new Date().toISOString() |
The timestamp to display. String values pass through to datetime as-is; Date/Dayjs/number values are normalized to ISO 8601. |
| format | string |
"MMM DD, YYYY" |
Format for the displayed text (and the title, when relative is true). See the dayjs display format docs. |
| relative | boolean |
false |
Display the timestamp relative to now (e.g. "4 days ago") instead of a formatted date. |
| withoutSuffix | boolean |
false |
Remove the "ago"/"in" suffix from relative time. Only applies when relative is true. |
| relativeStyle | RelativeStyle ("default" | "micro") |
"default" |
Render a compact single unit (e.g. "4d") instead of the humanized string. Only applies when relative is true. See Compact relative time. |
| live | boolean | number |
false |
Keep relative time updated. true uses the adaptive schedule (see Performance); a number sets a fixed interval in ms. Only applies when relative is true. |
| locale | Locales (TypeScript) | string |
"en" |
Locale used to format the timestamp. See supported locales and Internationalization. |
| tz | string |
undefined |
IANA timezone (e.g. "America/New_York") to render the timestamp in. Requires the dayjs utc/timezone plugins. See tz prop. |
| relativeThreshold | number |
undefined |
Switch from relative to the absolute format once the timestamp's age (ms) meets or exceeds this value. See Auto-switch to absolute format. |
| children | Snippet<[string]> |
undefined |
Custom markup rendered inside the time element instead of the plain formatted string; receives the formatted value as its argument. See Custom markup. |
Both the svelteTime action and the time attachment accept the same options as the Time component's props, plus:
| Name | Type | Default value | Description |
|---|---|---|---|
| title | string | undefined |
formatted timestamp (when relative is true) |
Override the title attribute; pass undefined to omit it entirely. |
live accepts the same boolean | number on both, but the timers differ. The svelteTime action uses a per-node fixed interval (60 seconds by default when true, or a custom ms value). The time attachment shares the adaptive global timer with the Time component. Those schedules are separate. See Performance.
The machine-readable datetime attribute is the accessible, parseable channel. The title tooltip isn't reachable via touch or keyboard, so don't put essential info only there; show the absolute date in text when it matters. Live text updates are not announced (aria-live is omitted on purpose). Minute-by-minute announcements would be hostile to screen-reader users.
| Name | Type | Default value |
|---|---|---|
| value | number | string | object | Duration |
0 (ignored when since is set) |
| unit | "milliseconds" | "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years" |
"milliseconds" (only applies when value is a plain number) |
| since | string | number | Date | Dayjs |
undefined (see live elapsed duration) |
| format | string |
"HH:mm:ss" |
| humanize | boolean |
false |
| withSuffix | boolean |
false (only applies when humanize is true) |
| locale | Locales (TypeScript) | string |
"en" (See supported locales) |
| live | boolean | number |
false (only applies when since is set) |
| children | Snippet<[string]> |
undefined |
| Name | Type | Default value |
|---|---|---|
| since | string | number | Date | Dayjs |
undefined (captured once at mount as "now"); changing it resets the stopwatch |
| running | boolean |
true; set to false to pause, excluding the paused interval from elapsed time |
| format | string |
"HH:mm:ss" |
| humanize | boolean |
false |
| withSuffix | boolean |
false (only applies when humanize is true) |
| locale | Locales (TypeScript) | string |
"en" (See supported locales) |
| live | boolean | number |
true (ticks every second while running; pass a number for a custom fixed interval in ms) |
| children | Snippet<[string, boolean]> |
undefined; receives the formatted value and the current running state |
| Name | Type | Default value |
|---|---|---|
| to | string | number | Date | Dayjs |
(required) target instant to count down to |
| format | string |
"HH:mm:ss" |
| humanize | boolean |
false |
| withSuffix | boolean |
false (only applies when humanize is true) |
| locale | Locales (TypeScript) | string |
"en" (See supported locales) |
| live | boolean | number |
true (ticks every second; pass a number for a custom fixed interval in ms) |
| oncomplete | () => void |
undefined; fires once, when the countdown reaches to |
| children | Snippet<[string, boolean]> |
undefined; receives the formatted value and a done flag |
| Name | Type | Default value |
|---|---|---|
| start | string | number | Date | Dayjs |
(required) start instant |
| end | string | number | Date | Dayjs |
(required) end instant |
| format | string |
"MMM DD, YYYY" (applied independently to start and end) |
| separator | string |
" – " |
| locale | Locales (TypeScript) | string |
"en" (See supported locales) |
| tz | string |
undefined (applied to both start and end; requires the dayjs utc/timezone plugins. See tz prop) |
| children | Snippet<[object]> |
undefined; receives a single object with formattedStart, formattedEnd, startDatetime, and endDatetime fields. Destructure what you need. Replaces the entire default output, not just the inner text |
The ...rest props (SvelteHTMLElements["time"] minus children) are spread onto both <time> elements. There is no per-side rest-prop split in v1.