Skip to content

sokaluis/react-date-range

Repository files navigation

@cyberlz/react-date-range

Maintained fork/rescue of react-date-range. Current channel: 1.2.x is the stable release line on npm latest. It adds DatePickerInput, DateRangeInput, and named range slots (Range.label) on top of the 1.1.x accessibility/RTL/cross-month baseline. The rc tag remains on 1.0.0-rc.0 for historical validation. See Dist-tag policy below.


What is this?

This is a personal open-source initiative to rescue and modernize react-date-range, a popular React date range picker that has been archived by its original maintainer.

Not affiliated with the original maintainers, any commercial project, or any company. This workspace exists on a personal machine for convenience; it carries no branding or association beyond what is stated here.

Why?

react-date-range has 2,600+ GitHub stars, 700+ forks, and broad real-world usage, but the upstream repository is read-only and no longer maintained. Open issues include:

  • React 19 compatibility (#661, #662)
  • date-fns v3/v4 compatibility (#649, #663, #667)
  • TypeScript improvements (#260, #439, #513)
  • Accessibility (#373, #415/#416 — labels, roles, focus-visible states, and live-region announcements resolved)
  • RTL layout support (#669)
  • Cross-month passive-day range selection UX (#495)
  • React 18 StrictMode scroll bugs (#577, #653)

There are community forks, but none clearly active in 2026 with a published React 19 fix. This project aims to fill that gap.

Install

npm install @cyberlz/react-date-range
import { DatePickerInput, DateRangePicker } from '@cyberlz/react-date-range';
import '@cyberlz/react-date-range/styles.css';
import '@cyberlz/react-date-range/theme/default.css';

Migrating from react-date-range upstream? See docs/migration-from-upstream.md.

Accessibility baseline

The fork now covers the core ARIA labels and states tracked by upstream #415/#416:

Area Behavior
Calendar grid Named with ariaLabels.calendar and described with ariaLabels.calendarRoleDescription.
Defined ranges Static range buttons expose aria-pressed for the active preset.
Input ranges Number-of-days inputs are named by their rendered labels via aria-labelledby.
Date display The start/end date inputs are grouped with role="group" and ariaLabels.dateDisplay. Per-range labels are available via Range.label — when set, the per-range wrapper renders as a named role="group" with aria-labelledby.
DatePickerInput Read-only trigger input opens a named role="dialog" popover, reflects aria-expanded, closes on Escape/outside click/date selection, and returns focus to the trigger.
DateRangePicker Wrapper renders as role="region" named by ariaLabels.dateRangePicker; set ariaLabels.dateRangePicker = false to opt out.
Calendar live region Committed month/year navigation announced via aria-live="polite" region (customizable via ariaLabels.liveRegionMonthYear). Hover, drag movement, date selection, and scroll do not announce from Calendar itself.
DateRange live region Committed range selections announced via aria-live="polite" / aria-atomic="true" after DateRange normalizes the selected range. Customizable via ariaLabels.liveRegionSelection. Hover, preview, and drag movement do not announce.

aria-live month/year and DateRange selection announcements are available. Announcements are tied to committed state changes only, so hover, preview, and drag-move updates do not over-announce.

Multi-range with labels

Assign label to each Range to name range slots for users and assistive tech:

const [ranges, setRanges] = useState([
  { startDate: new Date(), endDate: new Date('2026-07-14'), key: 'trip1', label: 'Trip 1' },
  { startDate: new Date('2026-08-01'), endDate: new Date('2026-08-07'), key: 'trip2', label: 'Trip 2' },
]);

<DateRangePicker ranges={ranges} onChange={({ trip1, trip2 }) => setRanges([trip1, trip2])} />

When label is set, DateDisplay renders each range inside role="group" aria-labelledby={id} so screen readers announce the label as the group's accessible name. Labels are rendered as plain text and are XSS-safe by design.

Input-trigger date picker

Use DatePickerInput when the UI needs a compact input trigger instead of an always-inline Calendar. The trigger is read-only in this first slice: users pick from the calendar popover, and manual text parsing is deferred.

import { useState } from 'react';
import { DatePickerInput } from '@cyberlz/react-date-range';

function TripDateField() {
  const [date, setDate] = useState(new Date());

  return (
    <DatePickerInput
      date={date}
      onChange={setDate}
      ariaLabel="Trip date"
      popoverLabel="Choose trip date"
      placeholder="Select a date"
      calendarProps={{ months: 1 }}
    />
  );
}

Controlled popover state is also supported via open, defaultOpen, and onOpenChange:

<DatePickerInput
  date={date}
  onChange={setDate}
  open={isOpen}
  onOpenChange={setIsOpen}
  ariaLabel="Controlled trip date"
/>

Input-trigger date range picker

Use DateRangeInput when the UI needs a compact input trigger for range selection instead of an always-inline DateRangePicker. The trigger is read-only: users pick from the calendar popover, and manual text parsing is deferred.

import { useState } from 'react';
import { DateRangeInput } from '@cyberlz/react-date-range';

function TripRangeField() {
  const [ranges, setRanges] = useState([
    { startDate: new Date(), endDate: new Date('2026-07-14'), key: 'trip' },
  ]);

  return (
    <DateRangeInput
      ranges={ranges}
      onChange={({ trip }) => setRanges([trip])}
      ariaLabel="Trip date range"
      popoverLabel="Choose trip dates"
      triggerPlaceholder="Select trip dates"
      calendarProps={{ months: 1 }}
    />
  );
}

Controlled popover state is also supported via open, defaultOpen, and onOpenChange:

<DateRangeInput
  ranges={ranges}
  onChange={({ trip }) => setRanges([trip])}
  open={isOpen}
  onOpenChange={setIsOpen}
  ariaLabel="Controlled trip date range"
/>

RTL layout support

Calendar, DateRange, and DateRangePicker accept an additive dir prop:

<DateRangePicker dir="rtl" direction="horizontal" />
dir value Behavior
"rtl" Renders dir="rtl", applies the rdrRtl class hook, mirrors navigation glyphs visually, and reverses horizontal month flow.
"ltr" Renders dir="ltr" without the RTL class hook.
omitted Leaves dir unset so the calendar can inherit direction from an ancestor.

The existing direction prop still controls layout orientation ("vertical" or "horizontal"); it is separate from text direction. Consumers can override the RTL hook with classNames={{ rtl: 'my-rtl' }}.

Custom navigatorRenderer output is not wrapped or transformed by the library. If a custom renderer uses chevrons, alignment, or directional icons, it must handle RTL mirroring itself.

Goal

A modern, maintained, production-ready date range picker for React that:

  • Works with React 18 and 19 without warnings or workarounds
  • Is fully typed (TypeScript-first)
  • Has a modern build (ESM + CJS, real tree-shaking, no side-effect imports)
  • Is SSR-safe
  • Preserves the existing API so current users can upgrade without rewrites
  • Keeps the compatibility surface focused on the maintained react-date-range experience

Current phase

Phase Status
Phase 0 — Audit & planning Complete
Phase 1 — Compatible rescue Complete
Phase 3 — Core refactor Complete (Slices 1–21 done)

@cyberlz/react-date-range@1.2.x is the stable release line on npm latest. 1.0/1.1/1.2 are stable, additive, and non-breaking. 1.2 adds DatePickerInput, DateRangeInput, and named range slots (Range.label) on top of the 1.1 accessibility, RTL, and cross-month selection baseline. Future work is tracked separately. See docs/fork-roadmap.md for the full plan and docs/refactor-roadmap.md for incremental refactor slices.

Dist-tag policy

npm has four relevant dist-tags for this package:

  • latest — points to the current 1.2.x stable release. Default install path: npm install @cyberlz/react-date-range.
  • rc — points to 1.0.0-rc.0. Historical release candidate for pre-release validation: npm install @cyberlz/react-date-range@rc.
  • beta — points to 0.1.0-beta.0. Legacy prerelease channel.
  • alpha — points to 0.1.0-alpha.3. Legacy prerelease channel.

For stable installs, consumers use npm install @cyberlz/react-date-range (no tag). See docs/migration-from-upstream.md for upgrade instructions. See docs/release-flow.md for the full policy.

Build pipeline

Package build produces consumable dist/ output:

npm run build          # tsdown (JS, multi-entry + unbundle) + sass (CSS) + types copy

Output:

Path Purpose
dist/index.mjs ESM barrel (bundlers, import)
dist/index.cjs CJS barrel (Node.js require)
dist/components/<Component>/index.{mjs,cjs} Per-component sub-modules (preserved by multi-entry glob for real tree-shaking)
dist/styles.css Compiled CSS (import separately)
dist/index.d.ts TypeScript declarations
dist/theme/default.css Default theme CSS

Consumer import (ESM / TSX):

import { DateRangePicker } from '@cyberlz/react-date-range';
import '@cyberlz/react-date-range/styles.css';

Consumer import (CJS):

const { DateRangePicker } = require('@cyberlz/react-date-range');
require('@cyberlz/react-date-range/styles.css');

No custom Vite/esbuild loaders required — the compiled output is plain JS/CSS.

Smoke-test fixtures:

  • spikes/consumer-js/ — JS consumer importing compiled output
  • spikes/consumer-tsx/ — TSX consumer with type validation (tsc --noEmit)
  • spikes/tree-shaking/ — Tree-shaking analysis (Calendar-only vs DateRangePicker)

Tree-shaking

Tree-shaking works since 0.1.0-alpha.3 and remains part of the stable 1.x line. The build uses tsdown with unbundle: true and a multi-entry glob, so each component is emitted as its own file and bundlers can drop unused exports. Verified empirically with spikes/tree-shaking/analyze.mjs:

Consumer import Output size
import { Calendar } ~41 KB
import { DateRangePicker } ~58 KB
Delta +17 KB (real tree-shaking)

CJS consumers do not benefit equally (CJS is not tree-shakeable by design); ESM + modern bundlers (Vite, Webpack 5, esbuild, Rollup) get the full benefit.

Not committed for 1.x

  • Breaking API changes
  • Speculative visual redesign tracks such as dual skins or Tailwind theming
  • Breaking feature tracks without a compatibility plan

Demo and documentation roadmap

  • Local demo: demo/ is the current verified Vite consumer. During development it resolves package imports to local src/ so unreleased fixes are exercised before publishing.
  • Public demo: https://sokaluis.github.io/react-date-range/ is deployed with GitHub Pages from demo/.
  • Full library documentation: planned after the landing baseline, covering component props, examples, migration notes, styling, accessibility, and roadmap status.

See docs/docs-site-plan.md for the landing/docs plan and docs/post-1.0-roadmap.md for the broader post-1.0 direction.

License

Upstream is MIT. This package is published as MIT and preserves upstream attribution in NOTICE.md.

Navigation

Links

About

Modern maintained fork of react-date-range with React 18/19 support, TypeScript types, and stable demo.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors