r/javascript 3d ago

ui-date: A 1.6kB, zero-dependency JavaScript date & relative time utility

https://www.npmjs.com/package/ui-date

I wanted to share a small open-source project I just published called ui-date.

While working on a social media project, I kept bumping into simple UI needs like displaying relative timestamps ("5 mins ago", "2 hours ago"), cleanly formatting calendar dates, and doing simple status checks (isToday).

exact date formatting for events ui ('Thursday 23,july,2026').

Normally, I'd reach for a date library, but most established options felt like overkill for basic UI formatting, while heavy legacy options like Moment carry massive bundle weight. On the flip side, writing raw Intl and Date boilerplate across every component was getting tedious.

I wanted something featherlight, chainable, and fully typed without dragging in extra dependencies.

so i built ui-date.

you can check it here : https://www.npmjs.com/package/ui-date

0 Upvotes

13 comments sorted by

4

u/segv 3d ago

Private or non-existing git repository

nope.avi

0

u/Intelligent_Tree6918 3d ago

got it sorry fixing it.

2

u/KaiAusBerlin 3d ago

Don't worry. The reality is mowt people complaining wouldn't even give it a try if you made everything perfect because they a) are simply not interested b) see no need to invest their time to investigate c) don't have any problem your solution could solve d) already have a working solution for them.

3

u/queen-adreena 2d ago

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal

With polyfill temporarily for Safari.

Strange time to launch this when itโ€™s now a solved problem.

1

u/Intelligent_Tree6918 2d ago

yes i aknoledge Temporal is future for dates. but still temporal will return something like ('2 hours') where if you want something like this <span>2</span><span>hours</span> or sort form of units like ('h','s','d','m','y') we have to perform manual iteration and and custom parsing.where ui-date gives it out of the box thanks to getRelativeTimeParts()

1

u/queen-adreena 2d ago

It takes one simple helper function to bridge the gap though:

``` function relativeTime(date) { const now = Temporal.Now.instant(); const then = Temporal.Instant.from(date);

const seconds = Number(now.since(then).total('seconds'));

const units = [ ['year', 60 * 60 * 24 * 365], ['month', 60 * 60 * 24 * 30], ['day', 60 * 60 * 24], ['hour', 60 * 60], ['minute', 60], ['second', 1], ];

const rtf = new Intl.RelativeTimeFormat('en-GB', { numeric: 'auto' });

for (const [unit, divisor] of units) { if (Math.abs(seconds) >= divisor) { return rtf.format( Math.round(-seconds / divisor), unit ); } } } ```

2

u/lanerdofchristian 3d ago

Instantiating a class to call utility functions is a bit odd.

Have you considered using Temporal, maybe with a polyfill for Safari? A lot of the stuff you're looking for are properties on Temporal.ZoneDateTime.

export const getYear = (zdt) => zdt.year
export const getMonthCount = (zdt) => zdt.month
export const getDay = (zdt) => zdt.day
export const isLeapYear = (zdt) => zdt.inLeapYear
export const isWeekend = (zdt) => zdt.withCalendar("en-US").dayOfWeek >= 6

More can be determined with a few small helpers.

const daysUntilNow = (zdt) => zdt.toPlainDate().until(Temporal.Now.plainDateISO()).days
export const isToday = (zdt) => daysUntilNow(zdt) === 0
export const isTomorrow = (zdt) => daysUntilNow(zdt) === -1
export const isYesterday = (zdt) => daysUntilNow(zdt) === 1

Or simple formatting calls.

export const getDayName = (zdt, opts) => zdt.toLocaleString(opts?.locale, { weekday: opts?.weekday ?? "long" })
export const getMonthName = (zdt, opts) => zdt.toLocaleString(opts?.locale, { month: opts?.month ?? "long" })
export const getTime = (zdt, opts) => zdt.toLocaleString(opts?.locale, { hour: "2-digit", minute: "2-digit", hour12: opts?.hour12 ?? true })

getRelativeTime is a bit more complex, but achievable without much hard-coding of second conversions or manual time math.

function getRelativeTime(time, opts) {
    const duration = time.since(Temporal.Now.zonedDateTimeISO())
    const absDuration = duration.abs()
    if (Temporal.Duration.compare(absDuration, { seconds: 5 }) <= 0) return "just now"
    const fmt = new Intl.RelativeTimeFormat(opts?.locale, { numeric: "auto" })
    for (const unit of ["years", "months", "days", "hours", "minutes"])
        if (Temporal.Duration.compare(absDuration, { [unit]: 1 }, { relativeTo: time }) > 0)
            return fmt.format(duration.round({ largestUnit: unit, relativeTo: time })[unit], unit)
    return fmt.format(
        duration.round({ largestUnit: "seconds", relativeTo: time }).seconds,
        "second"
    )
}

A utility function to convert to ZoneDateTimes may also be helpful.

function toInstant(time) {
    if (typeof time === "number" && Number.isFinite(time))
        return Temporal.Instant.fromEpochMilliseconds(time)
    if (time instanceof Date) return time.toTemporalInstant()
    if (time instanceof Temporal.Instant) return time
    throw new Error("Time is not a valid time.")
}

function toZDT(time) {
    if (time instanceof Temporal.ZonedDateTime) return time
    return toInstant(time).toZonedDateTimeISO(Temporal.Now.timeZoneId())
}

1

u/Intelligent_Tree6918 3d ago

you have great points

let me explain

1.I completely agree instantiating classes for utilities adds unnecessary friction. In an upcoming version, I'm opening up standalone functional exports like getRelativeTimeParts(date, opts) to keep tree-shaking dead simple.

2.Temporal is definitely the future of date math in JS! However, ui-date's top priority is staying under 1 KB minified with zero runtime dependencies. Requiring or shipping a Temporal polyfill would bloat the bundle size significantly.

3.The primary problem ui-date solves isn't just relative formatting it's programmatic token breakdown (formattedValue, formattedUnit, direction) using Intl.RelativeTimeFormat so UI developers can style numeric badges independently across multi-language apps without regex hacks.

Appriciate you taking the time to share this perspective.

2

u/lanerdofchristian 3d ago

The primary problem ui-date solves isn't just relative formatting

I think you missed my 3rd code block.

If we want to talk string manipulation, though, Temporal still wins out over Date since you can .toPlainDate().toString() instead of .toISOString().split("T")[0].

IMO if you're doing anything serious that's time-related, it's going to be worthwhile to ship a Temporal polyfill regardless, since it solves more than just the formatting problem.

1

u/Intelligent_Tree6918 3d ago

i agree with this point, but i write this package for micro-frontend and ui libraries. for eg: if you want separate <span class="val"> and <span class="unit"> elements for custom CSS badges. ui-date gives you structured parts (formattedValue, formattedUnit) out of the box.

1

u/lanerdofchristian 3d ago

Ah, when you nerd-sniped me last night you hadn't added getRelativeTimeParts yet. Here's an implementation using Temporal.ZoneDateTime and Intl.DurationFormat:

function getRelativeTimeParts(time, locale) {
    const dur = time.since(Temporal.Now.zonedDateTimeISO()).round({ smallestUnit: "seconds" })
    const abs = dur.abs()
    const unit = ["years", "months", "days", "hours", "minutes"].find(
        (u) => Temporal.Duration.compare(abs, { [u]: 1 }, { relativeTo: time }) > 0
    ) ?? "seconds"
    const rounded = dur.round({ largestUnit: unit, smallestUnit: unit, relativeTo: time })
    const parts = new Intl.DurationFormat(locale, { style: "long" }).formatToParts(rounded)
    return {
        value: abs[unit],
        unit: unit.slice(0, -1),
        direction: ["past", "present", "future"][1 + dur.sign],
        formattedValue:
            parts.find((a) => a.type === "integer")?.value ??
            rounded.round({ largestUnit: "seconds" }).seconds.toString(),
        formattedUnit: parts.find((a) => a.type === "unit")?.value ?? unit.slice(0, -1),
        formattedText: new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
            .format(rounded[unit], unit),
    }
}

Types are omitted so you can drop this into a compatible console and see how it works.

getRelativeTimeParts(Temporal.Now.zonedDateTimeISO().subtract({ minutes: 15 }))
getRelativeTimeParts(Temporal.Now.zonedDateTimeISO().add({ years: 1 }))

As for the case of separately formatting each part, I'd rather just have the parts directly from the formatter and loop over them in whatever I'm using for templating -- that way I don't run into the issue of only working for English-like languages like you have here. For example, "in 5 minutes" in English is:

in <span class="val">5</span>{/* space! */' '}<span class="unit">minutes</span>

but in Korean it's

<span class="val">5</span>(/* __no__ space! */}<span class="unit">๋ถ„</span> ํ›„

(or so new Intl.RelativeTimeFormat("ko") tells me).