nepali-date-kit
v2.0.0
Published
Complete Nepali (Bikram Sambat) date toolkit — a moment-style immutable date object, AD/BS conversion, formatting, and React date pickers and calendar.
Maintainers
Readme
🇳🇵 nepali-date-kit
The complete Bikram Sambat toolkit — a moment-style date object, AD ⇄ BS conversion, and React pickers and calendar, in one dependency-free package.
import nepaliDate from 'nepali-date-kit';
nepaliDate('2082-05-30').format('dddd, DD MMMM YYYY'); // "Monday, 30 Bhadra 2082"
nepaliDate('2082-05-30').toAd(); // "2025-09-15"
nepaliDate().add(1, 'month').fromNow(); // "in a month"Contents
- Install
- Why v2
- Quick start
- The
nepaliDateobject - Formatting
- Standalone functions
- React components
- React Native
- Browser / CDN
- Accuracy
- Migrating from v1
Install
npm install nepali-date-kitReact and React DOM are optional peer dependencies — install them only if you use the components. The core has no dependencies at all.
Why v2
v2 fixes three problems that affected every user of v1:
| Problem | What happened | Now |
| --- | --- | --- |
| The epoch was one day off | The converter anchored 1 Baisakh 2000 BS to 13 April 1943 instead of 14 April. Every single conversion was a day early, and the calendar grid patched over it with a +1 weekday offset — so dates and weekdays disagreed. | Anchored to 14 April 1943, verified against published calendars and round-tripped over all 33,247 supported days. |
| Conversions changed with your timezone | Dates were built with Date.UTC(...) and read back with local getters. Outside Nepal the result could land on the wrong day. | All calendar maths runs on integer day numbers. Verified byte-identical across 8 timezones from UTC−11 to UTC+14. |
| The pickers needed your Tailwind config | Components rendered Tailwind utility classes. If your app didn't have Tailwind scanning node_modules/nepali-date-kit, the popover lost its positioning and fell behind antd/MUI modals. | The stylesheet ships inside the bundle and is injected automatically. The panel is position: fixed with an explicit z-index. |
Plus: the calendar component is now actually Nepali (v1's rendered January–December), the react-native entry is no longer an empty file, and the published .d.ts no longer references types that were never shipped.
See Migrating from v1 for the breaking changes.
Quick start
import nepaliDate, { toAd, toBs, isAfter, format } from 'nepali-date-kit';
// Today, in BS
nepaliDate().format(); // e.g. "2082-05-30"
// Convert
toAd('2082-05-30'); // "2025-09-15"
toBs('2025-09-15'); // "2082-05-30"
toBs(new Date()); // today, in BS
// Compare — leave the second date out to compare with today
isAfter('2082-06-15'); // true
isAfter('2082-06-15', '2082-05-30'); // true
// Format, in English or Nepali
format('2082-05-30', 'DD MMMM YYYY'); // "30 Bhadra 2082"
format('2082-05-30', 'DD MMMM YYYY', 'bs', 'np'); // "३० भाद्र २०८२"The nepaliDate object
nepaliDate(input?) returns an immutable BS date. Every method that changes something returns a new instance, so chains never mutate what you started with.
const joined = nepaliDate('2082-05-30');
joined.add(1, 'year'); // a new instance
joined.format(); // "2082-05-30" — unchangedReading input
| Call | Reads the input as |
| --- | --- |
| nepaliDate('2082-05-30') | BS |
| nepaliDate({ year: 2082, month: 5, day: 30 }) | BS |
| nepaliDate.fromAd('2025-09-15') | AD |
| nepaliDate(new Date()) | AD (an absolute instant) |
| nepaliDate(1757900000000) | AD (epoch milliseconds) |
| nepaliDate(dayjsValue) / nepaliDate(momentValue) | AD (via $d / _d / toDate()) |
Bare strings are read as BS, because that is what this library is for. Date objects, epoch numbers and dayjs/moment values are absolute instants, so they are always AD. Separators -, / and . all work, a time component is optional, and Nepali digits (२०८२-०५-३०) are accepted.
Anything unreadable produces an invalid instance rather than an exception:
nepaliDate('nonsense').isValid(); // false
nepaliDate('nonsense').format(); // "Invalid Date"Getters and setters
Call with no argument to read, with an argument to get a new instance back.
nepaliDate('2082-05-30').year(); // 2082
nepaliDate('2082-05-30').month(); // 5 — 1 = Baisakh
nepaliDate('2082-05-30').date(); // 30
nepaliDate('2082-05-30').day(); // 1 — 0 = Sunday
nepaliDate('2082-05-30').year(2083).format(); // "2083-05-30"
nepaliDate('2082-05-30').month(1).format(); // "2082-01-30"
nepaliDate('2082-05-30').day(0).format(); // "2082-05-29" — the Sunday of that weekMonths are 1-indexed (1 = Baisakh … 12 = Chaitra), unlike moment's 0-indexed months. This keeps
.month()equal to whatYYYY-MM-DDprints.
Also available: hour(), minute(), second(), millisecond(), and the generic get(unit) / set(unit, value).
Arithmetic
nepaliDate('2082-12-30').add(1, 'day').format(); // "2083-01-01"
nepaliDate('2082-05-15').add(3, 'months').format(); // "2082-08-15"
nepaliDate('2082-05-15').subtract(1, 'year').format(); // "2081-05-15"
nepaliDate('2082-05-30 23:30').add(45, 'minutes').format('YYYY-MM-DD HH:mm'); // "2082-05-31 00:15"
nepaliDate('2082-05-15').add(1, 'quarter').format(); // "2082-08-15"Units accept every spelling: year/years/y, quarter/Q, month/months/M, week/w, day/days/d, hour/h, minute/m, second/s, millisecond/ms. As in moment, capital M is month and lowercase m is minute.
Adding months clamps the day when the target month is shorter — the 32nd of Asar plus a month is the last day of Shrawan, never a spill into Bhadra.
Boundaries
nepaliDate('2082-05-15').startOf('month').format(); // "2082-05-01"
nepaliDate('2082-05-15').endOf('month').format(); // "2082-05-31"
nepaliDate('2082-05-15').startOf('week').format(); // "2082-05-15" — that Sunday
nepaliDate('2082-05-15').endOf('day').format('HH:mm:ss.SSS'); // "23:59:59.999"startOf / endOf accept year, quarter, month, week, day, hour, minute, second.
Comparison
Every comparator defaults its second argument to now, and compares whole days unless you pass a unit.
nepaliDate('2082-06-15').isAfter(); // after today?
nepaliDate('2082-06-15').isAfter('2082-05-30'); // after that date?
nepaliDate('2082-06-15').isAfter('2082-05-30', 'month'); // a whole month later?
nepaliDate('2082-05-30').isBefore('2082-06-01');
nepaliDate('2082-05-30').isSame('2082-05-30');
nepaliDate('2082-05-30').isSameOrBefore('2082-05-30');
nepaliDate('2082-05-15').isBetween('2082-05-01', '2082-05-30'); // true
nepaliDate('2082-05-01').isBetween('2082-05-01', '2082-05-30'); // true — bounds included
nepaliDate('2082-05-01').isBetween('2082-05-01', '2082-05-30', 'day', '()'); // false — both excludedisBetween includes both bounds by default ('[]'), which differs from moment's exclusive '()'; pass the notation explicitly to choose. The standalone isBetween(date, from, to, unit?, inclusivity?) behaves identically.
Convenience predicates: isToday(), isTomorrow(), isYesterday(), isWeekend() (Saturday), isLeapYear() (a 366-day BS year), isValid().
Differences and relative time
nepaliDate('2082-06-01').diff('2082-05-01', 'day'); // 31
nepaliDate('2082-06-01').diff('2082-05-01', 'month'); // 1
nepaliDate('2082-06-01').diff('2082-05-01', 'day', true); // 31 (unrounded float)
nepaliDate().subtract(2, 'day').fromNow(); // "2 days ago"
nepaliDate().add(3, 'hour').fromNow(); // "in 3 hours"
nepaliDate().subtract(2, 'day').fromNow(false, 'np'); // "२ दिन अगाडि"
nepaliDate().calendar(); // "Today"
nepaliDate().add(1, 'day').calendar(); // "Tomorrow"
nepaliDate().subtract(3, 'day').calendar(); // "Last Friday" (whichever weekday that was)diff truncates toward zero, matching moment; pass true as the third argument for the exact fraction.
Output
const d = nepaliDate('2082-05-30 15:04:05');
d.format('YYYY-MM-DD'); // "2082-05-30"
d.toAd('DD MMMM YYYY'); // "15 September 2025"
d.toDate(); // native Date
d.toObject(); // { year: 2082, month: 5, day: 30, hour: 15, ... }
d.toArray(); // [2082, 5, 30, 15, 4, 5, 0]
d.toISOString(); // "2025-09-15T09:19:05.000Z" — from local time, so offset-dependent
d.valueOf(); // epoch ms
d.unix(); // epoch seconds
d.value; // "2082-05-30"
`${d}`; // "2082-05-30 15:04:05"Calendar queries
nepaliDate('2082-05-30').daysInMonth(); // 31
nepaliDate('2082-05-30').daysInYear(); // 365
nepaliDate('2082-05-30').dayOfYear(); // 155
nepaliDate('2082-05-30').weekOfYear(); // 23
nepaliDate('2082-05-30').quarter(); // 2Statics
nepaliDate.now(); // now
nepaliDate.today(); // today at 00:00
nepaliDate.fromAd('2025-09-15'); // read an AD value
nepaliDate.unix(1757900000); // from epoch seconds
nepaliDate.min('2082-05-30', '2081-01-01'); // earliest
nepaliDate.max(['2082-05-30', '2083-01-01']); // latest (array also works)
nepaliDate.isNepaliDate(value); // type guard
nepaliDate.minYear; // 2000
nepaliDate.maxYear; // 2090Durations
import { duration } from 'nepali-date-kit';
duration(90, 'minutes').asHours(); // 1.5
duration({ days: 2, hours: 6 }).asHours(); // 54
duration(90, 'minutes').humanize(); // "2 hours"
duration({ days: 1, hours: 2 }).toISOString(); // "P1DT2H"Formatting
format(template, lang?) on an instance, or format(input, template, calendar?, lang?) standalone. Templates use moment's tokens.
| Token | Output | Example |
| --- | --- | --- |
| YYYY YY | Year | 2082, 82 |
| M MM | Month number | 5, 05 |
| MMM MMMM | Month name | Bhd, Bhadra |
| D DD Do | Day of month | 30, 30, 30th |
| DDD DDDD | Day of year | 155, 155 |
| d dd ddd dddd | Weekday | 1, Mo, Mon, Monday |
| w ww | Week of year | 23, 23 |
| Q | Quarter | 2 |
| H HH | Hour, 24h | 15, 15 |
| h hh | Hour, 12h | 3, 03 |
| k kk | Hour, 1–24 | 15, 15 |
| m mm | Minute | 4, 04 |
| s ss | Second | 5, 05 |
| S SS SSS | Fractional second | 0, 07, 078 |
| A a | Meridiem | PM, pm |
| Z ZZ | UTC offset | +05:45, +0545 |
| X x | Unix seconds / ms | 1757… |
| [...] | Literal text | [on] YYYY → on 2082 |
Anything not a token passes through, so YYYY-MM-DD, DD/MM/YYYY and YYYY年MM月 all work. Wrap letters you want kept verbatim in square brackets.
Nepali output — pass 'np' as the language. Digits become Devanagari and names are translated; bracketed literals are left alone.
format('2082-05-30', 'DD MMMM YYYY, dddd', 'bs', 'np'); // "३० भाद्र २०८२, सोमबार"
format('2082-05-30', 'DD MMMM YYYY', 'ad', 'np'); // "१५ सेप्टेम्बर २०२५"Cross-calendar — the third argument picks which calendar to print. The input is always read as BS.
format('2082-05-30', 'YYYY-MM-DD', 'ad'); // "2025-09-15"Standalone functions
Every instance method has a function form, for when you're working with plain strings.
Conversion — toAd(input, template?, lang?), toBs(input, template?, lang?), toAdDate(input), format(input, template?, calendar?, lang?), parse(input, calendar?)
Comparison — isAfter, isBefore, isSame (alias isEqual), isSameOrAfter, isSameOrBefore, isBetween, isToday, isTomorrow, isYesterday, isWeekend, isLeapYear, isValidDate
isAfter(date1, date2?, precision?); // date2 defaults to today, precision to 'day'Arithmetic — add, subtract, diff, startOf, endOf, setDate, setMonth, setYear, addDaysFromToday, subtractDaysFromToday
Boundaries — startOfDay, endOfDay, getMonthStartDate, getMonthEndDate, getYearStartDate, getYearEndDate, getWeekStartDate, getWeekEndDate
Ranges and calendar data — getDatesBetween, daysBetween, getMonthDates, getSupportedYears, getWeekDay, getDaysInMonth, getDaysInYear, getCalendarMatrix
Relative time — fromNow, from, calendarTime, relativeTime, duration
getDatesBetween('2082-05-01', '2082-05-03'); // ["2082-05-01", "2082-05-02", "2082-05-03"]
daysBetween('2082-05-01', '2082-05-11'); // 10
getSupportedYears(); // [2000, 2001, ..., 2090]getCalendarMatrix
Build your own grid, with the same data the components use.
import { getCalendarMatrix } from 'nepali-date-kit';
const { weeks, cells, weekdays, monthName } = getCalendarMatrix({ year: 2082, month: 5 });
// weeks: 6 rows of 7 cells
// each cell: { date, year, month, day, weekday, ad, adDay, isCurrentMonth, isToday, isWeekend }Options: year, month, showOutsideDays (default true), weekStartsOn (default 0 = Sunday), fixedWeeks (default true, always 6 rows so the height never jumps).
React components
import { DatePicker, RangePicker, NepaliCalendar } from 'nepali-date-kit/react';Styles are injected automatically — no CSS import and no Tailwind config required. If you'd rather control it (server rendering, CSP), import nepali-date-kit/style.css instead.
<DatePicker />
const [date, setDate] = useState('');
<DatePicker value={date} onChange={setDate} color="#2563eb" theme="dark" allowAd />;onChange gives you a BS YYYY-MM-DD string, or '' when cleared.
<RangePicker />
const [range, setRange] = useState([]);
<RangePicker value={range} onChange={setRange} minDate="2082-01-01" />;onChange gives you [start, end], always in order even if you click the end first, or [] when cleared.
Picker props
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| value | string / string[] | — | BS YYYY-MM-DD. |
| onChange | (value) => void | — | |
| color | string | theme accent | Selected-day colour. Readable text and the range fill are derived from it. |
| theme | 'light' \| 'dark' \| 'auto' | 'light' | auto follows the OS. |
| tokens | Partial<TThemeTokens> | — | Override individual colours, radius or shadow. |
| className / style | | | Applied to the trigger. |
| panelClassName / panelStyle | | | Applied to the popover. |
| size | 'small' \| 'medium' \| 'large' | 'medium' | Trigger height. |
| minDate / maxDate | TInputDate | — | |
| disabledDate | (cell) => boolean | — | Reject individual dates. |
| allowAd | boolean | false | Show the AD day in each cell. |
| lang | 'en' \| 'np' | 'en' | |
| displayFormat | string | 'YYYY-MM-DD' | Any format template. |
| placeholder | string | 'Select date' | |
| allowClear | boolean | true | |
| disabled | boolean | false | |
| weekStartsOn | 0–6 | 0 | |
| placement | 'auto' \| 'top' \| 'bottom' | 'auto' | |
| zIndex | number | 9999 | |
| getPopupContainer | () => HTMLElement | document.body | |
| inputRender | (text, open) => ReactNode | — | Replace the trigger. |
| onOpenChange | (open) => void | — | |
| name | string | — | Emits a hidden input for plain HTML forms. |
Positioning
The panel is portalled to document.body, positioned fixed, flipped above the trigger when there isn't room below, and clamped to the viewport. zIndex defaults to 9999, which clears antd's modal (1000) and MUI's (1300). Raise it if your own overlay sits higher:
<DatePicker zIndex={20000} />Styling
Every colour is a CSS custom property, so className can override anything:
.my-picker {
--ndk-accent: #16a34a;
--ndk-radius: 4px;
--ndk-border: #cbd5e1;
}<DatePicker className="my-picker" />Available: --ndk-bg, --ndk-fg, --ndk-muted, --ndk-border, --ndk-hover, --ndk-accent, --ndk-accent-fg, --ndk-range-bg, --ndk-disabled, --ndk-weekend, --ndk-today-ring, --ndk-radius, --ndk-shadow.
<NepaliCalendar />
A full-month BS calendar with events.
<NepaliCalendar
size="large"
allowAd
events={[
{ date: '2082-05-12', title: 'Launch', color: '#22c55e' },
{ date: '2025-09-15', calendar: 'ad', title: 'From an AD date' },
]}
onClick={(cell) => console.log(cell.date, cell.ad)}
onEventClick={(event) => console.log(event.title)}
/>Sizes
| Size | Shows |
| --- | --- |
| small | The date, plus one coloured dot per event |
| medium | The above, plus 1 titled event chip |
| large | The above, plus up to 3 titled event chips |
Any events beyond the limit are summarised as +N more. Dots appear at every size, so a busy day is visible even in small.
Calendar props
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| events | TCalendarEvent[] | [] | { date, title?, color?, calendar?, isAd? }. date is BS unless calendar: 'ad'. |
| size | 'small' \| 'medium' \| 'large' | 'medium' | |
| allowAd | boolean | true | AD day in each cell, AD month span in the header. Set false to also hide events marked isAd. |
| onClick | (cell, event) => void | — | Any date cell click. |
| onEventClick | (event, cell) => void | — | Doesn't also fire onClick. |
| onSelect | (date) => void | — | |
| onMonthChange | ({ year, month }) => void | — | |
| value / defaultValue | string | — | Controlled / uncontrolled selection. |
| year / month | number | — | Controlled visible month. |
| renderCell | (cell, events) => ReactNode | — | Replace a cell's contents. |
| disabledDate | (cell) => boolean | — | |
| showHeader | boolean | true | |
| className / style / cellClassName | | | |
| color / theme / tokens / lang / weekStartsOn | | | As per the pickers. |
Works with your form library
The pickers are plain controlled components, so they drop into anything:
// react-hook-form
<Controller name="joined" control={control} render={({ field }) => <DatePicker {...field} />} />;
// Formik
<DatePicker value={values.joined} onChange={(v) => setFieldValue('joined', v)} />;
// antd Form
<Form.Item name="joined"><DatePicker /></Form.Item>;React Native
React Native has no DOM, and a calendar there should use your own design system. So this entry ships headless hooks — they do the calendar maths and state, you render with View / Text / Pressable.
import { useNepaliCalendar } from 'nepali-date-kit/react-native';
const cal = useNepaliCalendar({ value, onChange: setValue, lang: 'np' });
<View>
<Text>{cal.monthName} {cal.localizeDigits(cal.year)}</Text>
{cal.matrix.weeks.map((week, i) => (
<View key={i} style={{ flexDirection: 'row' }}>
{week.map((cell) => (
<Pressable key={cell.date} disabled={cal.isDisabled(cell)} onPress={() => cal.select(cell.date)}>
<Text style={cell.isToday && styles.today}>{cal.localizeDigits(cell.day)}</Text>
</Pressable>
))}
</View>
))}
<Button title="Next" onPress={cal.nextMonth} disabled={!cal.canGoNext} />
</View>;useNepaliCalendar returns matrix, year, month, monthName, weekdayNames, selected, today, nextMonth, prevMonth, goToToday, setMonth, select, isDisabled, localizeDigits, canGoNext, canGoPrev.
There's also useNepaliDate(initial?, displayFormat?, lang?) for when you just need a value holder, and the entire core API is re-exported from this entry.
Browser / CDN
<script src="https://cdn.jsdelivr.net/npm/nepali-date-kit@2"></script>
<script>
NepaliDateKit.toBs(new Date()); // "2082-05-30"
NepaliDateKit.nepaliDate().format('DD MMMM YYYY'); // "30 Bhadra 2082"
</script>The CDN build is the core utilities only — no React, no DOM requirement.
Accuracy
The calendar covers 2000–2090 BS (1943–2034 AD). Bikram Sambat month lengths are set by astronomical observation and published year by year, so they can't be computed; the table lists every year explicitly.
The epoch is 1 Baisakh 2000 BS = 14 April 1943 AD, a Wednesday, cross-checked against Hamro Patro, Ashesh and nepalicalendar. The test suite verifies:
- Every published New Year anchor from 2000 to 2083 BS, with its weekday.
- A BS → AD → BS round trip for all 33,247 supported days, confirming the AD date advances by exactly one day per BS day.
- Byte-identical output across 8 timezones from
Pacific/Midway(UTC−11) toPacific/Kiritimati(UTC+14), including both directions of a US DST transition.
Dates outside the range throw a descriptive RangeError from the converter functions, and produce an invalid instance from nepaliDate() — never a silently wrong answer.
Migrating from v1
v2 corrects a one-day error in every conversion. If you have BS dates stored in a database that were produced by v1, they are one day early and need shifting.
Otherwise, the breaking changes:
1. nepaliDate() returns an object, not a string.
nepaliDate(); // v1: "2082-05-30" v2: a chainable instance
nepaliDate().format(); // "2082-05-30"
nepaliDate().value; // "2082-05-30"
`${nepaliDate()}`; // "2082-05-30" — template literals still work
toBs(); // "2082-05-30" — still a plain string2. Format tokens now match moment.
| v1 | v2 |
| --- | --- |
| M → 05 | M → 5, MM → 05 |
| MM → Bhd | MMM → Bhd |
| MMM → Bhadra | MMMM → Bhadra |
| dd → Mon | dd → Mo, ddd → Mon |
| ddd → Monday | dddd → Monday |
So 'YYYY-M-DD' becomes 'YYYY-MM-DD', and 'MMM YYYY' becomes 'MMMM YYYY'.
3. Comparators take whole days by default and default to today.
isAfter('2082-06-15'); // v1: threw v2: compares with today
isAfter(a, b); // v2: ignores time-of-day
isAfter(a, b, true); // v2: exact, to the millisecond (the v1 `strict` flag still works)4. startOfDay / endOfDay return an instance, not a Date. Call .toDate() for the old value. nepaliDateFormat() still returns a Date.
5. Component renames. BsCalendar → NepaliCalendar (the old name is still exported). The calendar's props changed substantially — it renders a BS month now.
6. Types are real exports. v1's .d.ts referenced TInputDate and friends without shipping them, so consumers saw Cannot find name. Import them instead:
import type { TInputDate, TNepaliDate, TCalendarCell } from 'nepali-date-kit';7. No Tailwind. Remove nepali-date-kit from your Tailwind content globs and drop any dist/output.css import — styles ship with the components.
License
MIT © Nischal Adhikari
