Skip to content
Star on GitHub
DatesDate range picker
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Date range picker

Pick a start and end across two months, or use a preset. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add date-range-picker
From–
To–
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add date-range-picker

React

$npx designme add date-range-picker --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

A range picker with two months side by side and a list of ready-made ranges such as Last 7 days and This quarter. People click a start day and an end day, see the range fill in as they hover, and confirm with Apply. It suits reports and dashboards where an agent's results are read over a period.

  • When a report, chart or agent summary needs a start and end date.
  • When most people want a common range, like the last 30 days, in one click.
  • When a range tied to your product, such as "Since pricing launch", should sit next to the standard ones.
03

Usage

Import it and drive it from your own state.

Import

import { DateRangePicker } from '@/components/designme/date-range-picker/DateRangePicker';

Example

The demo above as a React component, 20 lines you can copy.

'use client';
// The preview from designme.dev, rebuilt from the React component: the range since the pricing launch fills in.
import { useState } from 'react';
import { DateRangePicker, DateRangePickerPresets, type DateRangePickerRange } from './DateRangePicker';
import { useScript } from '../lib/useScript';

const TODAY = new Date(2026, 9, 19), LAUNCH = new Date(2026, 8, 18);
const PRESETS = [...DateRangePickerPresets, { key: 'launch', label: 'Since pricing launch', range: (t: Date): [Date, Date] => [LAUNCH, t] }];

/* ---------- Date range picker: ready-made ranges, two months, a hover preview, then Apply ---------- */
export default function DateRangePickerDemo() {
  const [range, setRange] = useState<DateRangePickerRange>({ start: null, end: null });
  useScript(async ({ wait }) => { await wait(900); setRange({ start: LAUNCH, end: TODAY }); });
  return (
    <div style={{ width: '100%' }}>
      <DateRangePicker today={TODAY} defaultMonth={new Date(2026, 8, 1)} presets={PRESETS} value={range} onValueChange={setRange} />
    </div>
  );
}
04

Props

Everything DateRangePicker takes.

PropTypeWhat it does
valueDateRangePickerRangeThe picked range. Days are Dates at local midnight
defaultValueDateRangePickerRange
onValueChange(range: DateRangePickerRange) => void
monthDateThe month in the left calendar, as its first day; the right one shows the month after. Defaults to last month
defaultMonthDate
onMonthChange(month: Date) => void
presetsDateRangePickerPreset[]The side list. Defaults to the last 7 and 30 days and this quarter. The one matching the range lights up
todayDateRanges end today in the presets. Defaults to the real today; pass one when rendering on a server
onApply(range: { start: Date; end: Date }) => voidCalled with the range when Apply is pressed
format(date: Date) => stringHow a day reads in the From and To boxes. Defaults to "Sep 18, 2026"
localestringUsed for the month names, the weekday letters and the default format
weekStartsOnWeekStart1 starts the weeks on Monday (the default), 0 on Sunday
classNamestring
05

Code

The full source, as the CLI installs it.

DateRangePicker.tsxReact · 175 lines
'use client';
import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
import { addDays, addMonths, daysBetween, dayKey, formatMonth, fromIsoDay, isoDay, monthDays, moveDay, sameMonth, startOfDay, startOfMonth, startOfQuarter, weekdayIndex, weekdayNames, type WeekStart } from '../lib/calendar';
import { cx } from '../lib/cx';
import { useControllable, useIsoLayoutEffect } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Swap } from '../lib/motion';
import '../designme.css';
import './date-range-picker.css';

/** A range of days. `end` is null while the second day is still to be picked */
export interface DateRangePickerRange {
  start: Date | null;
  end: Date | null;
}

/** A ready-made range in the side list, worked out from today */
export interface DateRangePickerPreset {
  key: string;
  label: string;
  range: (today: Date) => [Date, Date];
}

export interface DateRangePickerProps {
  /** The picked range. Days are Dates at local midnight */
  value?: DateRangePickerRange;
  defaultValue?: DateRangePickerRange;
  onValueChange?: (range: DateRangePickerRange) => void;
  /** The month in the left calendar, as its first day; the right one shows the month after. Defaults to last month */
  month?: Date;
  defaultMonth?: Date;
  onMonthChange?: (month: Date) => void;
  /** The side list. Defaults to the last 7 and 30 days and this quarter. The one matching the range lights up */
  presets?: DateRangePickerPreset[];
  /** Ranges end today in the presets. Defaults to the real today; pass one when rendering on a server */
  today?: Date;
  /** Called with the range when Apply is pressed */
  onApply?: (range: { start: Date; end: Date }) => void;
  /** How a day reads in the From and To boxes. Defaults to "Sep 18, 2026" */
  format?: (date: Date) => string;
  /** Used for the month names, the weekday letters and the default format */
  locale?: string;
  /** 1 starts the weeks on Monday (the default), 0 on Sunday */
  weekStartsOn?: WeekStart;
  className?: string;
}

/** The side list designme.dev starts from: the last 7 days, the last 30 days and this quarter */
export const DateRangePickerPresets: DateRangePickerPreset[] = [
  { key: '7', label: 'Last 7 days', range: t => [addDays(t, -6), t] },
  { key: '30', label: 'Last 30 days', range: t => [addDays(t, -29), t] },
  { key: 'q', label: 'This quarter', range: t => [startOfQuarter(t), t] }
];

const EMPTY: DateRangePickerRange = { start: null, end: null };

/** Two months side by side with ready-made ranges, a live preview of the range as you hover, and Apply. */
export function DateRangePicker({
  value, defaultValue = EMPTY, onValueChange, month, defaultMonth, onMonthChange, presets = DateRangePickerPresets, today: todayProp,
  onApply, format, locale = 'en-US', weekStartsOn = 1, className
}: DateRangePickerProps) {
  const [today] = useState(() => startOfDay(todayProp ?? new Date()));
  const now = todayProp ? startOfDay(todayProp) : today;
  const [range, setRange] = useControllable(value, defaultValue, onValueChange);
  const [view, setView] = useControllable(month, startOfMonth(defaultMonth ?? addMonths(defaultValue.end ?? now, -1)), onMonthChange);
  const [hover, setHover] = useState<Date | null>(null);
  const show = format ?? ((d: Date) => d.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }));
  const { start, end } = range;
  const ka = start ? dayKey(start) : 0, kb = end ? dayKey(end) : 0;
  const preset = start && end ? presets.find(p => { const [a, b] = p.range(now); return dayKey(a) === ka && dayKey(b) === kb; }) : undefined;

  // a range that matches a preset fills in day by day, the way picking the preset does on designme.dev
  const rangeKey = `${ka}-${kb}`;
  const [seen, setSeen] = useState(rangeKey);
  const [pass, setPass] = useState(0);
  const [flash, setFlash] = useState(false);
  if (seen !== rangeKey) { setSeen(rangeKey); setFlash(!!preset); if (preset) setPass(n => n + 1); }

  // when the range is set from outside, bring its last day into view
  const visible = (d: Date) => sameMonth(d, view) || sameMonth(d, addMonths(startOfMonth(view), 1));
  useEffect(() => { if (end && !visible(end)) setView(startOfMonth(addMonths(end, -1))); }, [kb]); // eslint-disable-line react-hooks/exhaustive-deps

  const click = (d: Date) => {
    setHover(null);
    setRange(!start || end || dayKey(d) < ka ? { start: d, end: null } : { start, end: d });
  };
  const choose = (p: DateRangePickerPreset) => {
    const [a, b] = p.range(now);
    setHover(null); setRange({ start: a, end: b });
    const base = startOfMonth(addMonths(b, -1));
    if (dayKey(base) !== dayKey(startOfMonth(view))) setView(base);
  };

  const [applied, setApplied] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
  useEffect(() => () => clearTimeout(timer.current), []);
  const apply = () => {
    if (!start || !end) return;
    onApply?.({ start, end });
    setApplied(true); clearTimeout(timer.current); timer.current = setTimeout(() => setApplied(false), 1200);
  };

  // arrow keys move the focus a day or a week, and Page Up / Page Down a month
  const cals = useRef<HTMLDivElement>(null);
  const [focus, setFocus] = useState<string | null>(null);
  useIsoLayoutEffect(() => {
    if (!focus) return;
    cals.current?.querySelector<HTMLButtonElement>(`[data-date="${focus}"]`)?.focus();
    setFocus(null);
  }, [focus, view]);
  // one day takes Tab: the one the arrows last reached, or the end, the start, today or the first day on show
  const [cursor, setCursor] = useState<Date | null>(null);
  const tabDay = [cursor, end, start, now].find(d => d && visible(d)) ?? startOfMonth(view);
  const keys = (e: KeyboardEvent<HTMLDivElement>) => {
    const at = (e.target as HTMLElement).dataset.date; if (!at) return;
    const next = moveDay(fromIsoDay(at), e.key, weekStartsOn, e.shiftKey); if (!next) return;
    e.preventDefault();
    if (!visible(next)) setView(startOfMonth(dayKey(next) < dayKey(view) ? next : addMonths(next, -1)));
    setCursor(next); setFocus(isoDay(next));
    if (start && !end) setHover(next);
  };

  // the days that light up when a preset fills in, in page order, for their stagger
  const reach = end ?? hover, kr = reach ? dayKey(reach) : 0;
  const months = [startOfMonth(view), startOfMonth(addMonths(startOfMonth(view), 1))];
  let lit = 0;
  const heads = weekdayNames(locale, weekStartsOn);

  return (
    <div data-dm="" className={cx('dm-drx dm-panel', className)}>
      <div className="dm-drx-side">
        {presets.map(p => <button type="button" key={p.key} className={cx('dm-drx-p', preset === p && 'dm-on')} aria-pressed={preset === p} onClick={() => choose(p)}>{p.label}</button>)}
      </div>
      <div className="dm-drx-main">
        <div className="dm-drx-top">
          <div className={cx('dm-drx-in', !start && 'dm-on')}><span className="dm-muted">From</span><b className="dm-drx-a"><Swap>{start ? show(start) : '–'}</Swap></b></div>
          <Icon name="right" size="xs" />
          <div className={cx('dm-drx-in', !!start && !end && 'dm-on')}><span className="dm-muted">To</span><b className="dm-drx-b"><Swap>{end ? show(end) : '–'}</Swap></b></div>
          <span className="dm-drx-n dm-muted">{start && end ? `${daysBetween(start, end) + 1} ${daysBetween(start, end) ? 'days' : 'day'}` : start ? 'Pick an end date' : 'Pick a start date'}</span>
        </div>
        <div ref={cals} className="dm-drx-cals" onMouseLeave={() => setHover(null)} onKeyDown={keys}>
          {months.map((mo, o) => (
            <div key={o} className="dm-drx-cal" data-o={o}>
              <h5>{formatMonth(mo, locale)}</h5>
              <div key={`${isoDay(mo)}-${pass}`} className="dm-drx-g">
                {heads.map((w, i) => <span key={'w' + i} className="dm-w" aria-hidden="true">{w}</span>)}
                {Array.from({ length: weekdayIndex(mo, weekStartsOn) }, (_, i) => <span key={'e' + i} className="dm-drx-d dm-e" />)}
                {monthDays(mo).map(d => {
                  const k = dayKey(d), isA = !!start && k === ka, isB = !!end && k === kb;
                  const between = !!start && !!reach && k > ka && k < kr;
                  const on = isA || isB || (between && !!end);
                  const stagger = flash && on ? { className: 'dm-enter', style: { animationDelay: lit++ * 8 + 'ms' } } : null;
                  return (
                    <button type="button" key={k} data-date={isoDay(d)} style={stagger?.style}
                      className={cx('dm-drx-d', isA && 'dm-a', isA && reach && kr > k && 'dm-ra', isB && 'dm-b dm-rb', between && (end ? 'dm-in' : 'dm-hv'),
                        !end && hover && start && k === kr && k > ka && 'dm-hv', stagger?.className)}
                      aria-pressed={isA || isB} aria-label={show(d)} tabIndex={k === dayKey(tabDay) ? 0 : -1}
                      onClick={() => click(d)} onMouseEnter={() => { if (start && !end) setHover(d); }}>{d.getDate()}</button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
        <div className="dm-drx-f">
          <button type="button" className="dm-pill dm-ghost dm-sm dm-drx-clr" onClick={() => { setHover(null); setRange(EMPTY); }}>Clear</button>
          <button type="button" className="dm-pill dm-primary dm-sm dm-drx-ok" disabled={!start || !end} onClick={apply}>
            {applied ? <><Icon name="check" size="xs" />Applied</> : 'Apply'}
          </button>
        </div>
      </div>
    </div>
  );
}
date-range-picker.cssReact · 31 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Date range picker ---------- */
.dm-drx{display:grid;grid-template-columns:160px 1fr;overflow:hidden}
@media (max-width:720px){.dm-drx{grid-template-columns:1fr}.dm-drx-side{flex-direction:row!important;flex-wrap:wrap;border-right:0!important;border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}}
.dm-drx-side{display:flex;flex-direction:column;gap:2px;padding:10px;border-right:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-drx-p{text-align:left;padding:7px 10px;border-radius:9px;font-size:12.5px;color:var(--dm-fg2);transition:background .2s,color .2s;white-space:nowrap}
@media (hover:hover) and (pointer:fine){.dm-drx-p:hover{background:var(--dm-row);color:var(--dm-fg)}}
.dm-drx-p.dm-on{background:var(--dm-sel);color:var(--dm-accent-fg,var(--dm-accent));font-weight:500}
.dm-drx-main{padding:var(--dm-s-3) 14px;display:flex;flex-direction:column;gap:10px;min-width:0}
.dm-drx-top{display:flex;align-items:center;gap:var(--dm-s-2);flex-wrap:wrap;color:var(--dm-muted)}
.dm-drx-in{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 var(--dm-s-3);border-radius:var(--dm-r-field);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);font-size:12px;white-space:nowrap;transition:background-color var(--dm-t) var(--dm-ease),box-shadow var(--dm-t) var(--dm-ease)}
.dm-drx-in b{font-weight:500;font-size:12.5px;color:var(--dm-fg)}
/* the end that the next click sets */
.dm-drx-in.dm-on{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
@supports (corner-shape:squircle){.dm-drx-in{corner-shape:squircle;border-radius:var(--dm-r-field-sq)}}
.dm-drx-n{margin-left:auto;font-size:12px}
.dm-drx-cals{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media (max-width:560px){.dm-drx-cals{grid-template-columns:1fr}.dm-drx-cal[data-o="1"]{display:none}}
.dm-drx-cal h5{margin:0 0 6px;font-size:12.5px;font-weight:500;text-align:center}
.dm-drx-g{display:grid;grid-template-columns:repeat(7,1fr);row-gap:2px}
.dm-drx-g .dm-w{font-size:10.5px;color:var(--dm-muted);text-align:center;height:22px}
.dm-drx-d{height:30px;font-size:12px;color:var(--dm-fg);position:relative;transition:background .15s,color .15s}
.dm-drx-d:empty{pointer-events:none}
/* the start and end keep their accent under the pointer, so their day stays readable */
@media (hover:hover) and (pointer:fine){.dm-drx-d:hover:not(.dm-e):not(.dm-a):not(.dm-b){background:var(--dm-chip);border-radius:var(--dm-r-sm)}}
.dm-drx-d.dm-in{background:var(--dm-sel);border-radius:0}
.dm-drx-d.dm-a,.dm-drx-d.dm-b{background:var(--dm-accent);color:var(--dm-on-accent);border-radius:var(--dm-r-sm);z-index:1}
.dm-drx-d.dm-a.dm-ra{border-radius:var(--dm-r-sm) 0 0 var(--dm-r-sm)}.dm-drx-d.dm-b.dm-rb{border-radius:0 var(--dm-r-sm) var(--dm-r-sm) 0}
.dm-drx-d.dm-hv{background:color-mix(in srgb,var(--dm-accent) 7%,transparent);border-radius:0}
.dm-drx-f{display:flex;justify-content:flex-end;gap:6px}
date-range-picker.htmlHTML · 13 lines
<div class="card wide" data-c="date-range-picker">
  <div class="stage-in">
    <div class="drx panel">
      <div class="drx-side"><button class="drx-p" data-p="7">Last 7 days</button><button class="drx-p" data-p="30">Last 30 days</button><button class="drx-p" data-p="q">This quarter</button><button class="drx-p" data-p="sp">Since pricing launch</button></div>
      <div class="drx-main">
        <div class="drx-top"><div class="drx-in"><span class="muted">From</span><b class="drx-a">–</b></div><svg class="i xs"><use href="#i-right"/></svg><div class="drx-in"><span class="muted">To</span><b class="drx-b">–</b></div><span class="drx-n muted"></span></div>
        <div class="drx-cals"><div class="drx-cal" data-o="0"></div><div class="drx-cal" data-o="1"></div></div>
        <div class="drx-f"><button class="pill ghost sm drx-clr">Clear</button><button class="pill primary sm drx-ok" disabled>Apply</button></div>
      </div>
    </div>
  </div>
</div>
date-range-picker.cssHTML · 30 lines
/* ---------- Date range picker ---------- */
.drx{display:grid;grid-template-columns:160px 1fr;overflow:hidden}
@media (max-width:720px){.drx{grid-template-columns:1fr}.drx-side{flex-direction:row!important;flex-wrap:wrap;border-right:0!important;border-bottom:1px solid color-mix(in srgb,var(--line) 50%,transparent)}}
.drx-side{display:flex;flex-direction:column;gap:2px;padding:10px;border-right:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.drx-p{text-align:left;padding:7px 10px;border-radius:9px;font-size:12.5px;color:var(--fg2);transition:background .2s,color .2s;white-space:nowrap}
@media (hover:hover) and (pointer:fine){.drx-p:hover{background:var(--row);color:var(--fg)}}
.drx-p.on{background:var(--sel);color:var(--accent-fg,var(--accent));font-weight:500}
.drx-main{padding:var(--s-3) 14px;display:flex;flex-direction:column;gap:10px;min-width:0}
.drx-top{display:flex;align-items:center;gap:var(--s-2);flex-wrap:wrap;color:var(--muted)}
.drx-in{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 var(--s-3);border-radius:var(--r-field);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line) 50%,transparent);font-size:12px;white-space:nowrap;transition:background-color var(--t) var(--ease),box-shadow var(--t) var(--ease)}
.drx-in b{font-weight:500;font-size:12.5px;color:var(--fg)}
/* the end that the next click sets */
.drx-in.on{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line-strong) 50%,transparent)}
@supports (corner-shape:squircle){.drx-in{corner-shape:squircle;border-radius:var(--r-field-sq)}}
.drx-n{margin-left:auto;font-size:12px}
.drx-cals{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media (max-width:560px){.drx-cals{grid-template-columns:1fr}.drx-cal[data-o="1"]{display:none}}
.drx-cal h5{margin:0 0 6px;font-size:12.5px;font-weight:500;text-align:center}
.drx-g{display:grid;grid-template-columns:repeat(7,1fr);row-gap:2px}
.drx-g .w{font-size:10.5px;color:var(--muted);text-align:center;height:22px}
.drx-d{height:30px;font-size:12px;color:var(--fg);position:relative;transition:background .15s,color .15s}
.drx-d:empty{pointer-events:none}
/* the start and end keep their accent under the pointer, so their day stays readable */
@media (hover:hover) and (pointer:fine){.drx-d:hover:not(.e):not(.a):not(.b){background:var(--chip);border-radius:var(--r-sm)}}
.drx-d.in{background:var(--sel);border-radius:0}
.drx-d.a,.drx-d.b{background:var(--accent);color:var(--on-accent);border-radius:var(--r-sm);z-index:1}
.drx-d.a.ra{border-radius:var(--r-sm) 0 0 var(--r-sm)}.drx-d.b.rb{border-radius:0 var(--r-sm) var(--r-sm) 0}
.drx-d.hv{background:color-mix(in srgb,var(--accent) 7%,transparent);border-radius:0}
.drx-f{display:flex;justify-content:flex-end;gap:6px}
date-range-picker.jsHTML · 64 lines
DM.def('date-range-picker', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{right}', I('right','xs'));
  const TODAY = new Date(2026, 9, 19); let base = new Date(2026, 8, 1), A = null, B = null, hov = null, cursor = null;
  const cals = $$('.drx-cal', r), ok = $('.drx-ok', r);
  const key = d => d.getFullYear() * 1e4 + d.getMonth() * 100 + d.getDate();
  const fmt = d => d ? d.toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }) : '–';
  /* From and To crossfade to the days picked, and always end on the latest ones: a pick that lands while the last one
     is still fading in (picking the same end again, two quick clicks, Enter twice) writes its day when the fade does */
  const put = (el, t) => { el._to = t; if (el._t || el.textContent === t) return; el.classList.add('sw', 'out');
    el._t = setTimeout(() => { el._t = 0; el.textContent = el._to; el.classList.remove('out'); }, REDUCE ? 0 : 170); };
  /* the days are built once per month view; selection and hover only change their classes */
  function build(){
    cals.forEach(cal => { const mo = new Date(base.getFullYear(), base.getMonth() + +cal.dataset.o, 1); const pad = (mo.getDay() + 6) % 7, days = new Date(mo.getFullYear(), mo.getMonth() + 1, 0).getDate();
      let h = `<h5>${mo.toLocaleDateString('en-US', { month:'long', year:'numeric' })}</h5><div class="drx-g">${['M','T','W','T','F','S','S'].map(w => `<span class="w">${w}</span>`).join('')}`;
      for (let i = 0; i < pad; i++) h += '<span class="drx-d e"></span>';
      for (let d = 1; d <= days; d++){ const dt = new Date(mo.getFullYear(), mo.getMonth(), d); h += `<button class="drx-d" data-t="${dt.getTime()}" data-k="${key(dt)}">${d}</button>`; }
      cal.innerHTML = h + '</div>'; });
    $$('.drx-d[data-t]', r).forEach(b => { b.onclick = () => click(new Date(+b.dataset.t)); b.onmouseenter = () => { if (A && !B){ hov = new Date(+b.dataset.t); deco(); } }; });
    deco();
  }
  function deco(){
    const end = B || hov, ka = A && key(A), kb = B && key(B), ke = end && key(end);
    $$('.drx-d[data-t]', r).forEach(b => { const k = +b.dataset.k; let cl = 'drx-d';
      if (A && k === ka) cl += ' a' + (end && ke > k ? ' ra' : '');
      if (B && k === kb) cl += ' b rb';
      if (A && end && k > ka && k < ke) cl += B ? ' in' : ' hv';
      if (!B && hov && A && k === ke && k > ka) cl += ' hv';
      if (b.className !== cl) b.className = cl; });
    // one day takes Tab: the one the arrows last reached, or the end, the start, today or the first day on show
    const days = $$('.drx-d[data-t]', r), on = [cursor, B, A, TODAY].map(d => d && days.find(b => +b.dataset.k === key(d))).find(Boolean) || days[0];
    days.forEach(b => { const t = b === on ? '0' : '-1'; if (b.getAttribute('tabindex') !== t) b.setAttribute('tabindex', t); });
    put($('.drx-a', r), fmt(A)); put($('.drx-b', r), fmt(B));
    const ins = $$('.drx-in', r); ins[0].classList.toggle('on', !A); ins[1].classList.toggle('on', !!A && !B);
    const n = A && B ? Math.round((B - A) / 864e5) + 1 : 0;
    $('.drx-n', r).textContent = n ? n + (n === 1 ? ' day' : ' days') : A ? 'Pick an end date' : 'Pick a start date';
    ok.disabled = !(A && B);
  }
  function click(d){ $$('.drx-p', r).forEach(x => x.classList.remove('on')); if (!A || B || d < A){ A = d; B = null; hov = null; } else { B = d; hov = null; } deco(); }
  $$('.drx-p', r).forEach(p => p.onclick = () => { $$('.drx-p', r).forEach(x => x.classList.toggle('on', x === p)); const v = p.dataset.p; B = new Date(TODAY);
    A = new Date(TODAY); if (v === '7') A.setDate(A.getDate() - 6); else if (v === '30') A.setDate(A.getDate() - 29); else if (v === 'q') A = new Date(2026, 9, 1); else A = new Date(2026, 8, 18);
    const nb = new Date(B.getFullYear(), B.getMonth() - 1, 1); hov = null; if (+nb !== +base){ base = nb; build(); } else deco(); stagger($$('.drx-d.in, .drx-d.a, .drx-d.b', r), 8); });
  $('.drx-clr', r).onclick = () => { A = B = hov = null; $$('.drx-p', r).forEach(x => x.classList.remove('on')); deco(); };
  ok.onclick = () => { ok.innerHTML = I('check','xs') + 'Applied'; c.later(1200, () => ok.textContent = 'Apply'); };
  c.on($('.drx-cals', r), 'mouseleave', () => { if (hov){ hov = null; deco(); } });
  // the keyboard in the calendars: the arrows move a day or a week, Home and End go to the start and end of the week, Page Up
  // and Page Down move a month, or a year with Shift, and the months on show follow; Enter or Space picks the day. While the
  // end is open, the range follows the focus as it does the pointer
  c.on($('.drx-cals', r), 'keydown', e => {
    const b = e.target.closest('.drx-d[data-t]'); if (!b) return;
    const d = new Date(+b.dataset.t), wd = (d.getDay() + 6) % 7, at = n => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
    const mon = (y, mo) => new Date(y, mo, Math.min(d.getDate(), new Date(y, mo + 1, 0).getDate()));
    const n = { ArrowLeft:at(-1), ArrowRight:at(1), ArrowUp:at(-7), ArrowDown:at(7), Home:at(-wd), End:at(6 - wd),
      PageUp:e.shiftKey ? mon(d.getFullYear() - 1, d.getMonth()) : mon(d.getFullYear(), d.getMonth() - 1),
      PageDown:e.shiftKey ? mon(d.getFullYear() + 1, d.getMonth()) : mon(d.getFullYear(), d.getMonth() + 1) }[e.key];
    if (!n) return; e.preventDefault(); cursor = n;
    const m0 = new Date(n.getFullYear(), n.getMonth(), 1), last = new Date(base.getFullYear(), base.getMonth() + 1, 1);
    if (A && !B && n > A) hov = n;
    if (m0 < base){ base = m0; build(); } else if (m0 > last){ base = new Date(m0.getFullYear(), m0.getMonth() - 1, 1); build(); } else deco();
    const to = $(`.drx-d[data-k="${key(n)}"]`, r); if (to) to.focus({ preventScroll:true });
  });
  build();
  c.later(900, () => $('.drx-p[data-p="sp"]', r).click());
});
06

Questions

What people ask about it.

How do I add a custom preset like "Since launch"?
Spread the exported DateRangePickerPresets into a new array and add your own entry with a key, a label and a range function that takes today and returns [start, end].
How do people get to other months?
There are no previous and next buttons. The view moves when a preset is picked, when the keyboard focus passes the edge of the two months (Page Up and Page Down move a month), or when you set month yourself.
What is the difference between onValueChange and onApply?
onValueChange fires on every click, including when only the start is set and end is null. onApply fires only when the person presses Apply with both days set.
Which months show first?
By default the left calendar shows the month before the range's end, or before today, and the right one shows the month after it. Pass defaultMonth or month to choose.
07

More in Dates

Pickers for single dates and date ranges.

Date pickerA field that opens a calendar, with quick picks and natural language.
Segmented controlSwitch between views of the same thing, with a sliding indicator.
Event calendarA week view where the agent finds a free slot for you.
AgendaToday's schedule with a live now line and agent prep notes.