Nepali Date Picker - Web Components

Framework-agnostic Bikram Sambat (Nepali) date pickers as native custom elements. Every variant below is the same one you can drop into React, Vue, Angular, Svelte, or plain HTML. Calendar math comes from the Kotlin-compiled engine, so dates match the Android and Python builds exactly.

Controls apply to every picker below.
<nepali-date-picker>

Inline calendar

The core inline month calendar with keyboard navigation and localization.

Default
With English date + range limits (Baisakh–Chaitra 2081)
Disabled
HTML
<script type="module">import '@nepali-date-picker/web-component';</script>

<nepali-date-picker value="2081-05-24" show-english></nepali-date-picker>
<script>
  document.querySelector('nepali-date-picker')
    .addEventListener('change', (e) => console.log(e.detail));
</script>
calendar-system / show-calendar-toggle

Bikram Sambat or Gregorian

Every calendar element can display either calendar. Only the display changes: value, start, end and the change event stay Bikram Sambat, so switching keeps the same day selected.

With the B.S. / A.D. switch
Gregorian-first, no switch
Range picker, switchable
Wheel, switchable
Field typed in Gregorian
Docked, switchable
HTML
<!-- a switch the user can flip -->
<nepali-date-picker value="2083-06-01" show-calendar-toggle></nepali-date-picker>

<!-- or open straight on the Gregorian grid -->
<nepali-date-picker value="2083-06-01" calendar-system="ad"></nepali-date-picker>
<script>
  // The payload is Bikram Sambat whichever calendar is on screen.
  picker.addEventListener('change', (e) => console.log(e.detail.bsIso));
</script>
show-adjacent-month-days

Neighbouring months in the empty cells

Fills the blank cells around the month with the days either side of it, drawn faded. Clicking one picks that day and moves the grid to its month. Off by default, so an existing grid is unchanged.

Off (the default)
On
On, with the switch and both calendars
Range picker, filled edges
Docked, filled edges
HTML
<nepali-date-picker value="2083-04-15" show-adjacent-month-days></nepali-date-picker>
<nepali-calendar>

Browsing a month

The read-a-month element rather than the pick-a-date one: both calendars' numbers and the neighbouring months' days on by default, the picked day written out, and the month's events listed with a span gathered into one line. An entry's id and payload come back untouched, which is where an app keeps its own record, image links included.

The grid alone
With the day and the month written out
What the last click reported
Click a day, or a line of the month's list.
HTML
<nepali-calendar
  show-day-summary
  show-month-events
  weekly-off-days="7"
  events='[{"date":"2083-06-10","days":4,"name":"Indra Jatra","kind":"religious",
            "id":"indra-jatra","payload":"{\"imageUrl\":\"…\"}"}]'>
</nepali-calendar>
<script>
  document.querySelector('nepali-calendar')
    .addEventListener('event-select', (e) => {
      // The payload is yours: parse it and render the image with your own <img>.
      console.log(JSON.parse(e.detail.event.payload));
    });
</script>
show-secondary-date

Both calendars in every cell

The displayed calendar's day stays large and its counterpart sits small in the corner of the same cell, with the months the other calendar straddles named under the header. The web twin of NepaliDatePickerWithEnglishDate in Compose. Off by default, and the switch flips which calendar is the large one.

Off (the default)
On
On, with the switch and the neighbouring months
Range picker, paired
Docked, paired
Dialog, with the Gregorian date under the headline
HTML
<nepali-date-picker value="2083-06-02" show-secondary-date></nepali-date-picker>
<nepali-date-picker-dialog>

Dialog

A modal picker with confirm / cancel. Add fullscreen for the full-screen variant.

HTML
<button onclick="dlg.show()">Pick a date</button>
<nepali-date-picker-dialog id="dlg" value="2081-05-24"></nepali-date-picker-dialog>
<script>dlg.addEventListener('change', (e) => console.log(e.detail));</script>
<nepali-date-picker-docked>

Docked (field + popover)

Type a date or open the calendar in an anchored popover.

HTML
<nepali-date-picker-docked label="Appointment date"></nepali-date-picker-docked>
<nepali-date-range-picker>

Range picker

Pick a start then an end; the span between is shaded.

HTML
<nepali-date-range-picker start="2081-05-10" end="2081-05-18"></nepali-date-range-picker>
<script>
  document.querySelector('nepali-date-range-picker')
    .addEventListener('change', (e) => console.log(e.detail.startBsIso, e.detail.endBsIso));
</script>
<nepali-date-field>

Text field

Type a date (YYYY/MM/DD, Devanagari digits accepted) with inline validation. No calendar, so it drops into forms.

HTML
<nepali-date-field label="Date of birth"></nepali-date-field>
<script>
  field.addEventListener('change', (e) => console.log('valid', e.detail.bsIso));
  field.addEventListener('invalid', (e) => console.log('invalid', e.detail.message));
</script>
<nepali-date-range-field>

Range text fields

Two validated fields with a start ≤ end check.

Bikram Sambat
Typed in Gregorian, reported in Bikram Sambat
HTML
<nepali-date-range-field></nepali-date-range-field>
<nepali-wheel-date-picker>

Wheel picker

Three scrolling columns; the day column resizes to the month. Scroll, click, or use the arrow keys.

HTML
<nepali-wheel-date-picker value="2081-05-24"></nepali-wheel-date-picker>
events & weekly-off-days

Marking days

A colour says what the day is; dots say what is scheduled on it. A weekly off day repeats fifty-two times a year, so it is coloured and never dotted, which leaves all three dot slots for an app's own events.

Office week (Saturday off)
School week (Saturday + Sunday)
Dots for your own events
HTML
<nepali-date-picker
  weekly-off-days="7"
  events='[
    {"date":"2081-05-26","name":"Constitution Day","kind":"governmentPublic"},
    {"date":"2081-05-28","name":"Standup","indicate":true,"color":"#42a5f5"}
  ]'>
</nepali-date-picker>
endDate & days

An event that runs longer than a day

One entry, many days: give it endDate (included) or days and every day of the span is marked the same way, across a month or a year end. A span is capped at 366 days, and an end before the start marks the one day rather than disappearing.

A festival, by its end date
A week of leave, by a day count
HTML
<nepali-date-picker
  events='[
    {"date":"2081-06-17","endDate":"2081-06-26","name":"Dashain","kind":"religious"},
    {"date":"2081-07-02","days":5,"name":"Tihar","kind":"religious"}
  ]'>
</nepali-date-picker>
every calendar element

The same marking everywhere

events and weekly-off-days are attributes of every element that draws a grid, so a docked field, a range picker and a dialog mark their days identically. The colours come from CSS custom properties, so a brand palette is six lines of CSS.

Docked
Range
Dialog
Recoloured (CSS custom properties)
CSS
nepali-date-picker.brand-marks {
  --ndp-weekly-off: #8e24aa;
  --ndp-holiday-public: #d32f2f;
  --ndp-holiday-religious: #1e88e5;
  --ndp-holiday-regional: #00897b;
  --ndp-holiday-observance: #6d4c41;
  --ndp-holiday-container: rgba(211, 47, 47, 0.12);
}
@nepali-date-picker/core

Asking what a day is

The picker draws a colour and dots and no text. Everything else is a query on a policy: what the day is, what the month holds, and how many working days lie between two dates. Pick a day in the calendar to inspect it.

Weekly off: -
Non-working: -
Strongest kind: -
Named on it: -
Days in month: -
Closed days this month: -
Entries this month: -
Working days in 21: -
Next working day: -
+5 working days: -
JavaScript
import {
  createDetailedEvent, expandEventDays, createCalendarPolicy,
} from '@nepali-date-picker/core';

const dashain = createDetailedEvent(2081, 6, 17, 'Dashain', 'religious', true, 'dashain', null);
const office = createCalendarPolicy([7], expandEventDays(dashain, 10));

office.statusOf(2081, 6, 18).isNonWorking;          // true
office.monthStatus(2081, 6).filter((d) => d.isNonWorking).length;
office.workingDaysBetween(2081, 6, 1, 2081, 6, 22); // end exclusive
office.nextWorkingDay(2081, 6, 17);                 // steps over the whole span
min / max / disabled

Bounds, disabled and validation

min and max are Bikram Sambat YYYY-MM-DD strings, and every element that takes a date takes them: a day outside the window is drawn disabled rather than hidden, and a typed one is rejected. disabled dims the whole element and stops it responding. A field that cannot parse what is in it fires invalid instead of change, and carries the reason.

Field bounded to Bhadra–Kartik 2081 -
Docked, same window
Range picker, same window
Wheel, disabled
Docked, disabled
Range fields, disabled
HTML
<nepali-date-field label="Delivery date" min="2081-05-01" max="2081-07-30"></nepali-date-field>
<script>
  field.addEventListener('change', (e) => console.log('valid', e.detail.bsIso));
  field.addEventListener('invalid', (e) => console.log('rejected', e.detail.message));
</script>
label / start-label / heading / language

Labels, headings and language

Every visible string is an attribute. A range field names its two sides separately with start-label and end-label, a dialog takes a heading, and language is per element, so one form can hold a Nepali field next to an English one without a wrapper deciding for both.

Named range fields
Always Nepali, whatever the toolbar says
Always English, whatever the toolbar says
Dialog with a heading
Full-screen, titled and in Nepali
HTML
<nepali-date-range-field start-label="Leave from" end-label="Leave until"></nepali-date-range-field>
<nepali-date-picker-dialog heading="When should we deliver?"></nepali-date-picker-dialog>
<nepali-date-field label="जन्म मिति" language="ne"></nepali-date-field>
CSS custom properties

Restyling a single element

The elements draw from custom properties rather than a stylesheet of their own, and custom properties inherit through the shadow boundary. Setting them on one element restyles just that one; setting them on :root restyles the page. Nothing here needs a build step, a theme object, or a part selector.

Untouched
Every token overridden
The same tokens on a docked field
CSS
.themed-picker {
  --ndp-font: 'Iowan Old Style', Georgia, serif;
  --ndp-bg: #12262a;
  --ndp-text: #e7f5f4;
  --ndp-muted: #7fa8a6;
  --ndp-accent: #ffb703;
  --ndp-on-accent: #12262a;
  --ndp-hover: rgba(255, 183, 3, 0.16);
  --ndp-in-range: rgba(255, 183, 3, 0.2);
  --ndp-today-ring: #ffb703;
  --ndp-border: #21454a;
  --ndp-error: #ff8fa3;
  --ndp-radius: 20px;
}
@nepali-date-picker/core

Conversion engine (no UI)

The same package powering the pickers, usable directly in Node or the browser with no DOM at all. Change either date below and every group recomputes.

JavaScript
import {
  convertAdToBs, convertBsToAd, getTodayBs, getTodayAd, getCurrentTime,
  getBsMonth, getAdMonth, getTotalDaysInBsMonth, getBsDaysBetween, compareBsDates,
  getBsCalendarsInAdMonth, getAdCalendarsInBsMonth, isAdDateConvertible,
  formatBsDate, formatBsDateByPattern, formatAdDateByPattern,
  formatTimeEnglish, formatTimeNepali, bsDateTimeToIso, bsDateTimeFromIso,
  localizeDigits, toLatinDigits, getBsYearRange, getAdYearRangeForBsYears,
} from '@nepali-date-picker/core';

convertAdToBs(2024, 9, 9);              // { year: 2081, month: 5, dayOfMonth: 24, ... }
getBsMonth(2081, 5).totalDaysInMonth;   // 31
formatBsDateByPattern('EEEE, dd MMMM yyyy', 2081, 5, 24, 'en');
bsDateTimeFromIso(bsDateTimeToIso(2081, 5, 24, 9, 30, 0, 0));
integration

Use it in any framework

Custom elements are standard DOM, so every framework can render them.

import '@nepali-date-picker/web-component';

<nepali-date-picker value="2081-05-24"></nepali-date-picker>
import '@nepali-date-picker/web-component';

export function Picker() {
  return (
    <nepali-date-picker
      value="2081-05-24"
      onChange={(e) => console.log(e.nativeEvent.detail)}
    />
  );
}
// React wraps the CustomEvent, so the payload is on nativeEvent.
// Import '@nepali-date-picker/web-component/react' once for TSX types.
// vite.config.js: mark the tag as a custom element
vue({ template: { compilerOptions: { isCustomElement: (t) => t === 'nepali-date-picker' } } })

<script setup>import '@nepali-date-picker/web-component';</script>
<template>
  <nepali-date-picker value="2081-05-24" @change="(e) => console.log(e.detail)" />
</template>
// Add CUSTOM_ELEMENTS_SCHEMA to the module/component, then:
import '@nepali-date-picker/web-component';

<nepali-date-picker [attr.value]="value" (change)="onChange($event)"></nepali-date-picker>
<script>import '@nepali-date-picker/web-component';</script>

<!-- Svelte 5 -->
<nepali-date-picker value="2081-05-24" onchange={(e) => console.log(e.detail)} />

<!-- Svelte 4 -->
<nepali-date-picker value="2081-05-24" on:change={(e) => console.log(e.detail)} />
<!-- no bundler: one self-contained file, 64 kB gzipped -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@nepali-date-picker/web-component"></script>

<nepali-date-picker id="picker"></nepali-date-picker>

<script>
  // jQuery needs no plugin; .val() does not reach a custom element property
  $('#picker')[0].value = '2081-05-24';
  $('#picker').on('change', (e) => console.log(e.detail.bsIso));
</script>