BW DatePicker

Zero-dependency datepicker built for engineers who care about craft. Production-ready. Plugin-powered. Framework-agnostic.

v1.0.0 ~27KB gzipped 0 Dependencies JavaScript MIT License

Live Examples

These are actual BW DatePicker instances running on this page. Click on dates, navigate months, and interact with them.

Light Theme (Inline)

Always-visible calendar with light theme. Select dates directly.

Light Theme — Inline Mode
Selected: none
javascript
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';

const picker = new BWDatePicker('#date-input', {
  mode: 'inline',
  container: '#calendar-container'
}).use(ThemingPlugin, { theme: 'light' });

picker.on('date:changed', ({ date }) => {
  console.log('Selected:', date);
});

Dark Theme (Inline)

Black & white monochrome dark theme. The signature BW look.

Dark Theme — Inline Mode
Selected: none
javascript
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';

const picker = new BWDatePicker('#date-input', {
  mode: 'inline'
}).use(ThemingPlugin, { theme: 'dark' });

Theme Comparison

Side-by-side light and dark themes for direct comparison.

Light vs Dark
Light
Dark
javascript
// Light theme
const lightPicker = new BWDatePicker('#light-input', {
  mode: 'inline'
}).use(ThemingPlugin, { theme: 'light' });

// Dark theme
const darkPicker = new BWDatePicker('#dark-input', {
  mode: 'inline'
}).use(ThemingPlugin, { theme: 'dark' });

With Date Constraints

Limit selectable dates with min/max. Past dates are disabled.

Date Constraints — Future Dates Only
Past dates are disabled
javascript
const picker = new BWDatePicker('#date-input', {
  mode: 'inline',
  minDate: new Date(),  // Today onwards only
  maxDate: new Date('2026-12-31')
});

Popup Mode

Click the input field to open the calendar as a popup.

Popup — Click Input to Open
javascript
const picker = new BWDatePicker('#date-input', {
  mode: 'popup',  // Default mode
  closeOnSelect: true
});

picker.on('date:changed', ({ date }) => {
  console.log('Selected:', date);
});

Overview

Most datepickers are bloated, hard to customize, or locked into a framework. BW DatePicker is different — it respects your bundle size, works anywhere, and lets you use only what you need through its powerful plugin architecture.

🪶
Lightweight

Core is only ~27KB gzipped — add only what you need

🔌
Plugin Architecture

10+ official plugins for theming, accessibility, mobile & more

🎨
Themeable

Dark mode, light mode, auto-detect, CSS variables

Accessible

WCAG 2.1 compliant, full keyboard navigation

📱
Mobile Ready

Touch gestures, swipe navigation, responsive design

🚀
Zero Dependencies

No external libraries. No supply chain risk.

Installation

npm / yarn / pnpm

bash
# Core only
npm install @bw-ui/datepicker

# With recommended plugins
npm install @bw-ui/datepicker @bw-ui/datepicker-theming @bw-ui/datepicker-accessibility @bw-ui/datepicker-positioning

CDN

No build step required. Add directly to your HTML:

html
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/@bw-ui/datepicker/dist/bw-datepicker.min.css">

<!-- Script -->
<script src="https://unpkg.com/@bw-ui/datepicker/dist/bw-datepicker.min.js"></script>

Available Packages

Package Description Size
@bw-ui/datepicker Core datepicker engine ~27.3KB
@bw-ui/datepicker-react React components & hooks ~5.1KB
@bw-ui/datepicker-vue Vue 3 components ~4.8KB

Quick Start

Basic Usage (3 lines)

javascript
import { BWDatePicker } from '@bw-ui/datepicker';
import '@bw-ui/datepicker/dist/bw-datepicker.css';

const picker = new BWDatePicker('#date-input');

That's it. No config required. It just works.

With Plugins (Production Setup)

javascript
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';
import { AccessibilityPlugin } from '@bw-ui/datepicker-accessibility';
import { PositioningPlugin } from '@bw-ui/datepicker-positioning';
import { MobilePlugin } from '@bw-ui/datepicker-mobile';

// Styles
import '@bw-ui/datepicker/dist/bw-datepicker.css';
import '@bw-ui/datepicker-theming/dist/bw-theming.css';

// Create picker with plugins
const picker = new BWDatePicker('#date-input', {
  mode: 'popup',
  format: 'YYYY-MM-DD',
  minDate: new Date()
})
  .use(ThemingPlugin, { theme: 'auto' })
  .use(AccessibilityPlugin)
  .use(PositioningPlugin, { autoFlip: true })
  .use(MobilePlugin);

// Listen for changes
picker.on('date:changed', ({ date, dateISO }) => {
  console.log('Selected:', dateISO);
});

Usage Examples

javascript
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';

const picker = new BWDatePicker('#date-input', {
  mode: 'popup',
  format: 'YYYY-MM-DD'
}).use(ThemingPlugin, { theme: 'dark' });

picker.on('date:changed', ({ dateISO }) => {
  console.log('Selected:', dateISO);
});
html
<!-- Styles -->
<link rel="stylesheet" href="https://unpkg.com/@bw-ui/datepicker/dist/bw-datepicker.min.css">
<link rel="stylesheet" href="https://unpkg.com/@bw-ui/datepicker-theming/dist/bw-theming.min.css">

<!-- Scripts -->
<script src="https://unpkg.com/@bw-ui/datepicker/dist/bw-datepicker.min.js"></script>
<script src="https://unpkg.com/@bw-ui/datepicker-theming/dist/bw-theming.min.js"></script>

<!-- Your HTML -->
<input type="text" id="date-input" placeholder="Select a date">

<!-- Initialize -->
<script>
  const picker = new BW.BWDatePicker('#date-input')
    .use(BWTheming.ThemingPlugin, { theme: 'dark' });

  picker.on('date:changed', function(e) {
    console.log('Selected:', e.dateISO);
  });
</script>
jsx
import { useState } from 'react';
import { BWDatePicker } from '@bw-ui/datepicker-react';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';

// Styles
import '@bw-ui/datepicker/dist/bw-datepicker.css';
import '@bw-ui/datepicker-theming/dist/bw-theming.css';

function App() {
  const [date, setDate] = useState(null);

  return (
    <BWDatePicker
      value={date}
      onChange={setDate}
      plugins={[ThemingPlugin]}
      pluginOptions={{ theming: { theme: 'dark' } }}
      placeholder="Select a date"
      format="YYYY-MM-DD"
    />
  );
}

With Date Constraints

javascript
const picker = new BWDatePicker('#date', {
  mode: 'popup',
  format: 'DD/MM/YYYY',
  
  // Only allow future dates
  minDate: new Date(),
  
  // Until end of next year
  maxDate: new Date('2026-12-31'),
  
  // Block specific dates (holidays, etc.)
  disabledDates: [
    new Date('2025-01-01'),  // New Year
    new Date('2025-12-25'), // Christmas
    new Date('2025-12-26')  // Boxing Day
  ]
});

Options

Pass options as the second argument to the constructor:

javascript
new BWDatePicker(selector, options);
Option Type Default Description
mode 'popup' | 'modal' | 'inline' 'popup' How the datepicker is displayed
format string 'YYYY-MM-DD' Date format for display and input
minDate Date | null null Minimum selectable date
maxDate Date | null null Maximum selectable date
disabledDates Date[] [] Array of dates that cannot be selected
firstDayOfWeek 0-6 0 First day of week (0 = Sunday)
closeOnSelect boolean true Close picker after date selection

Methods

Lifecycle

javascript
picker.open();      // Open the datepicker
picker.close();     // Close the datepicker
picker.toggle();    // Toggle open/close
picker.destroy();   // Destroy instance and cleanup
picker.refresh();   // Re-render the datepicker

Date Operations

javascript
picker.setDate(date);   // Set the selected date
picker.getDate();       // Get the selected date (Date object)
picker.getDateISO();    // Get date as ISO string
picker.clear();         // Clear the selection
picker.today();         // Set to today's date

Navigation

javascript
picker.prevMonth();       // Navigate to previous month
picker.nextMonth();       // Navigate to next month
picker.prevYear();        // Navigate to previous year
picker.nextYear();        // Navigate to next year
picker.goToDate(date);   // Navigate to specific date

Plugin Management

javascript
picker.use(Plugin, options);   // Add a plugin
picker.hasPlugin('name');      // Check if plugin is loaded
picker.getPlugin('name');      // Get plugin instance

Events

Subscribe to events using .on():

javascript
picker.on('eventName', (data) => { /* handler */ });

Date Events

Event Payload Description
date:changed { date, dateISO, oldDate } Date was changed
date:selected { date, dateISO } User selected a date
date:cleared {} Date was cleared

Lifecycle Events

Event Payload Description
picker:opened {} Datepicker was opened
picker:closed {} Datepicker was closed
picker:destroyed {} Instance was destroyed

Navigation Events

Event Payload Description
nav:monthChanged { month, year } Month was changed
nav:yearChanged { year } Year was changed

Example

javascript
const picker = new BWDatePicker('#date');

picker.on('date:changed', ({ date, dateISO, oldDate }) => {
  console.log('New date:', dateISO);
  console.log('Previous date:', oldDate);
});

picker.on('picker:opened', () => {
  console.log('Datepicker is now open');
});

picker.on('nav:monthChanged', ({ month, year }) => {
  console.log(`Viewing: ${month}/${year}`);
});

Plugins

BW DatePicker has a fully modular plugin architecture. Install only what you need.

Theming Plugin

Dark mode, light mode, and auto-detection with full CSS variable support.

@bw-ui/datepicker-theming ~11.5KB
Theme Switching — Actual BW DatePicker
Light
Dark
javascript
import { ThemingPlugin } from '@bw-ui/datepicker-theming';
import '@bw-ui/datepicker-theming/dist/bw-theming.css';

picker.use(ThemingPlugin, {
  theme: 'auto',     // 'light' | 'dark' | 'auto'
  persist: true,    // Remember preference
  darkClass: 'dark' // CSS class for dark mode
});

Accessibility Plugin

Full keyboard navigation, ARIA attributes, and screen reader support. WCAG 2.1 compliant.

@bw-ui/datepicker-accessibility ~18.8KB
Keyboard Navigation — Actual BW DatePicker
Navigate Enter Select Esc Close
javascript
import { AccessibilityPlugin } from '@bw-ui/datepicker-accessibility';

picker.use(AccessibilityPlugin);

// Keyboard shortcuts enabled:
// ← → ↑ ↓  Navigate days
// Enter    Select date
// Escape   Close picker
// PageUp   Previous month
// PageDown Next month

Positioning Plugin

Smart popup positioning with collision detection and auto-flip when near viewport edges.

@bw-ui/datepicker-positioning ~11.2KB
Placement Options
bottom-start
Dec 2025
1
2
3
4
5
6
7
top-end
Dec 2025
1
2
3
4
5
6
7
javascript
import { PositioningPlugin } from '@bw-ui/datepicker-positioning';

picker.use(PositioningPlugin, {
  placement: 'bottom-start',  // Where to position
  autoFlip: true,             // Flip if no space
  offset: [0, 8]              // [x, y] offset
});

// Available placements:
// top, top-start, top-end
// bottom, bottom-start, bottom-end
// left, left-start, left-end
// right, right-start, right-end

Mobile Plugin

Touch gestures, swipe navigation, and mobile-optimized interactions.

@bw-ui/datepicker-mobile ~7.5KB
Swipe Gestures — Swipe left/right to change months
December 2025
SuMoTuWeThFrSa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
← Swipe or drag to change months →
javascript
import { MobilePlugin } from '@bw-ui/datepicker-mobile';

picker.use(MobilePlugin, {
  swipeThreshold: 50,    // Min swipe distance (px)
  preventScroll: true    // Lock body scroll when open
});

// Features:
// • Swipe left/right for months
// • Touch-optimized hit areas
// • Prevents body scroll

Range Plugin

Select a date range with start and end dates. Visual highlighting included.

@bw-ui/datepicker-range ~9KB
Date Range — Actual BW DatePicker
Select start and end dates
javascript
import { RangePlugin } from '@bw-ui/datepicker-range';

picker.use(RangePlugin, {
  presets: ['today', 'last7days', 'last30days', 'thisMonth'],
  presetsPosition: 'left'
});

picker.on('range:complete', ({ startDate, endDate, nights }) => {
  console.log('Check-in:', startDate);
  console.log('Check-out:', endDate);
  console.log('Nights:', nights);
});

Multidate Plugin

Select multiple individual dates. Perfect for booking multiple days.

@bw-ui/datepicker-multidate ~8KB
Multi Selection — Actual BW DatePicker
Click dates to toggle (max 5)
javascript
import { MultidatePlugin } from '@bw-ui/datepicker-multidate';

picker.use(MultidatePlugin, {
  maxDates: 5,        // Maximum selections
  separator: ', '     // Display separator
});

// Get all selected dates
const dates = picker.getDates();  // Returns Date[]

Locale Plugin

Internationalization and translations for different languages and regions.

@bw-ui/datepicker-locale ~8KB
Localization — Actual BW DatePicker
English (default)
Chinese (中文)
javascript
import { LocalePlugin } from '@bw-ui/datepicker-locale';

picker.use(LocalePlugin, {
  locale: 'de-DE',
  firstDayOfWeek: 1,  // Monday
  translations: {
    today: 'Heute',
    clear: 'Löschen',
    close: 'Schließen'
  }
});

Input Handler Plugin

Manual input with masking, validation, and format enforcement as you type.

@bw-ui/datepicker-input-handler ~12.2KB
Input Masking — Type a date
Type a date or click to open picker:
javascript
import { InputHandlerPlugin } from '@bw-ui/datepicker-input-handler';

picker.use(InputHandlerPlugin, {
  format: 'DD/MM/YYYY',
  allowManualInput: true,
  mask: true,        // Auto-format as typing
  strictMode: false  // Allow partial dates
});

Date Utils Plugin

Natural language date parsing and advanced manipulation.

@bw-ui/datepicker-date-utils ~19KB
Natural Language Parsing
javascript
import { DateUtilsPlugin } from '@bw-ui/datepicker-date-utils';

picker.use(DateUtilsPlugin);

// Parse natural language
picker.parseDate('tomorrow');
picker.parseDate('next friday');
picker.parseDate('in 2 weeks');
picker.parseDate('dec 25');
picker.parseDate('end of month');

Dual Calendar Plugin

Side-by-side month view, perfect for selecting date ranges.

@bw-ui/datepicker-dual-calendar ~10KB
Side-by-Side Months — Actual BW DatePicker
Select a date range
javascript
import { DualCalendarPlugin } from '@bw-ui/datepicker-dual-calendar';
import { RangePlugin } from '@bw-ui/datepicker-range';

picker
  .use(DualCalendarPlugin, { months: 2, linked: true })
  .use(RangePlugin);

// Perfect for hotel bookings, flights, etc.

Create Your Own Plugin

BW DatePicker has a fully open plugin architecture. Build anything you need.

Plugin Structure

javascript
const MyPlugin = {
  name: 'my-plugin',  // Unique identifier
  
  init(api, options) {
    // Called when plugin is registered
    // Return cleanup object (optional)
    return { /* instance data */ };
  },
  
  destroy(instance) {
    // Called when datepicker is destroyed
    // Clean up your plugin
  }
};

// Usage
picker.use(MyPlugin, { /* options */ });

The API Object

Your plugin receives full access to datepicker internals:

javascript
init(api, options) {
  // Core instance
  api.datepicker             // The BWDatePicker instance
  
  // DOM Elements
  api.getPickerElement()     // The picker container element
  api.getInputElement()      // The input element
  
  // Systems
  api.getEventBus()          // Event system for listening/emitting
  api.getStateManager()       // Internal state management
  
  // Configuration
  api.getOptions()            // User-provided options
}

Example: Highlight Today Plugin

javascript
export const HighlightTodayPlugin = {
  name: 'highlight-today',
  
  init(api, options = {}) {
    const eventBus = api.getEventBus();
    const color = options.color || '#ff6b6b';
    
    const highlightToday = () => {
      const today = api.getPickerElement()
        ?.querySelector('.bw-datepicker__day--today');
      
      if (today) {
        today.style.backgroundColor = color;
        today.style.color = 'white';
        today.style.borderRadius = '50%';
      }
    };
    
    eventBus.on('picker:opened', highlightToday);
    eventBus.on('nav:monthChanged', highlightToday);
    
    return { color, highlightToday };
  }
};

// Usage
picker.use(HighlightTodayPlugin, { color: '#10b981' });

React Integration

Use BW DatePicker in your React application with a simple wrapper component. The vanilla JS core ensures minimal bundle size while React handles the component lifecycle.

React Wrapper Component
tsx
import { useEffect, useRef } from 'react';
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';
import { RangePlugin } from '@bw-ui/datepicker-range';
import '@bw-ui/datepicker/dist/bw-datepicker.min.css';
import '@bw-ui/datepicker-theming/dist/bw-theming.min.css';
import '@bw-ui/datepicker-range/dist/bw-range.min.css';

interface DatePickerProps {
  onChange?: (date: Date | null) => void;
  onRangeChange?: (start: Date, end: Date) => void;
  theme?: 'light' | 'dark';
  enableRange?: boolean;
  placeholder?: string;
}

export function DatePicker({
  onChange,
  onRangeChange,
  theme = 'light',
  enableRange = false,
  placeholder = 'Select date'
}: DatePickerProps) {
  const inputRef = useRef<HTMLInputElement>(null);
  const pickerRef = useRef<BWDatePicker | null>(null);

  useEffect(() => {
    if (!inputRef.current) return;

    // Initialize picker
    const picker = new BWDatePicker(inputRef.current, {
      mode: 'popup',
      format: 'YYYY-MM-DD'
    });

    // Apply plugins
    picker.use(ThemingPlugin, { theme });
    
    if (enableRange) {
      picker.use(RangePlugin, {
        presets: ['today', 'last7days', 'thisMonth']
      });
      picker.on('range:complete', (e) => {
        onRangeChange?.(e.startDate, e.endDate);
      });
    } else {
      picker.on('date:changed', (e) => {
        onChange?.(e.date);
      });
    }

    pickerRef.current = picker;

    // Cleanup on unmount
    return () => {
      picker.destroy();
    };
  }, [theme, enableRange]);

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder={placeholder}
      className="bw-datepicker-input"
    />
  );
}
Usage in React App
tsx
import { DatePicker } from './DatePicker';

function BookingForm() {
  const handleDateChange = (date: Date | null) => {
    console.log('Selected:', date);
  };

  const handleRangeChange = (start: Date, end: Date) => {
    console.log('Check-in:', start);
    console.log('Check-out:', end);
  };

  return (
    <div>
      {/* Single date picker */}
      <DatePicker
        onChange={handleDateChange}
        theme="light"
        placeholder="Pick a date"
      />

      {/* Range picker for bookings */}
      <DatePicker
        enableRange
        onRangeChange={handleRangeChange}
        theme="dark"
        placeholder="Select dates"
      />
    </div>
  );
}

Vue Integration

Integrate BW DatePicker into Vue 3 applications using the Composition API. The wrapper component handles initialization and cleanup automatically.

Vue 3 Component
vue
<!-- BWDatePicker.vue -->
<template>
  <input
    ref="inputRef"
    type="text"
    :placeholder="placeholder"
    class="bw-datepicker-input"
  />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { BWDatePicker } from '@bw-ui/datepicker';
import { ThemingPlugin } from '@bw-ui/datepicker-theming';
import { RangePlugin } from '@bw-ui/datepicker-range';
import '@bw-ui/datepicker/dist/bw-datepicker.min.css';
import '@bw-ui/datepicker-theming/dist/bw-theming.min.css';
import '@bw-ui/datepicker-range/dist/bw-range.min.css';

interface Props {
  theme?: 'light' | 'dark';
  enableRange?: boolean;
  placeholder?: string;
}

const props = withDefaults(defineProps<Props>(), {
  theme: 'light',
  enableRange: false,
  placeholder: 'Select date'
});

const emit = defineEmits<{
  change: [date: Date | null];
  rangeChange: [start: Date, end: Date];
}>();

const inputRef = ref<HTMLInputElement | null>(null);
let picker: BWDatePicker | null = null;

const initPicker = () => {
  if (!inputRef.value) return;

  picker = new BWDatePicker(inputRef.value, {
    mode: 'popup',
    format: 'YYYY-MM-DD'
  });

  picker.use(ThemingPlugin, { theme: props.theme });

  if (props.enableRange) {
    picker.use(RangePlugin, {
      presets: ['today', 'last7days', 'thisMonth']
    });
    picker.on('range:complete', (e) => {
      emit('rangeChange', e.startDate, e.endDate);
    });
  } else {
    picker.on('date:changed', (e) => {
      emit('change', e.date);
    });
  }
};

onMounted(() => {
  initPicker();
});

onUnmounted(() => {
  picker?.destroy();
});

// Re-initialize when theme changes
watch(() => props.theme, () => {
  picker?.destroy();
  initPicker();
});
</script>
Usage in Vue App
vue
<!-- BookingForm.vue -->
<template>
  <div class="booking-form">
    <!-- Single date picker -->
    <BWDatePicker
      @change="handleDateChange"
      theme="light"
      placeholder="Pick a date"
    />

    <!-- Range picker for bookings -->
    <BWDatePicker
      enable-range
      @range-change="handleRangeChange"
      theme="dark"
      placeholder="Select dates"
    />
  </div>
</template>

<script setup lang="ts">
import BWDatePicker from './BWDatePicker.vue';

const handleDateChange = (date: Date | null) => {
  console.log('Selected:', date);
};

const handleRangeChange = (start: Date, end: Date) => {
  console.log('Check-in:', start);
  console.log('Check-out:', end);
};
</script>

Browser Support

Browser Version
Chrome 60+
Firefox 60+
Safari 12+
Edge 79+

Comparison

Feature BW DatePicker react-datepicker flatpickr
Zero dependencies
Modular plugins
Framework agnostic
React bindings
Full keyboard nav ⚠️
Mobile swipe
Custom plugins ⚠️
WCAG 2.1 ⚠️ ⚠️
Core size (gzipped) ~27KB ~40KB ~16KB