rangeflowv1.0.5
rangeflow v1.0.5 - now on npm
 
6 Days
 
Apr 06Apr 10Apr 14Apr 18Apr 22Apr 26Apr 30May 04

Date Ranges,
That Finally Flow.

Built for products that move through time.

RangeFlow combines a smooth date slider, preset ranges, and a popover calendar in one component, with TypeScript support and styling that stays inside the picker.

 
6 Days
 
Apr 06Apr 08Apr 10Apr 12Apr 14Apr 16Apr 18Apr 20
Transactions1014 Apr20 Apr 2026
Total spend272.53$321.59 USD
TodayMon · 20 Apr
  • Whole Foods06:04groceriesPending
    −€28.59$33.74
SaturdaySat · 18 Apr
  • New Balance14:27sport
    −€40.14$47.37
  • Lyft13:56transport
    −€7.99$9.43
  • Chipotle04:38food
    −€19.96$23.55
FridayFri · 17 Apr
  • Whole Foods13:02groceries
    −€25.21$29.75
ThursdayThu · 16 Apr
  • New Balance12:13sport
    −€58.51$69.04
  • Walmart09:08groceries
    −€31.40$37.05
WednesdayWed · 15 Apr
  • Amtrak05:23transport
    −€26.76$31.58
TuesdayTue · 14 Apr
  • New Balance19:18sport
    −€24.30$28.67
  • Uber10:35transport
    −€9.67$11.41

Install

One package. Zero global styles.

RangeFlow ships a single component and a single CSS file scoped to .rangeflow-date-picker. Import once at the root of your app. Nothing leaks.

$ npm install rangeflow
app/layout.tsx
// app/layout.tsx
import 'rangeflow/style.css'

Quick start

Two dates. One callback.

The defaultRange prop sets the visible window. The defaultSelected prop is what the user picks inside it. The onChangecallback fires with the new selection. That's the whole API surface for the common case.

components/activity.tsx
import { RangeFlow } from 'rangeflow'
import 'rangeflow/style.css'
import dayjs from 'dayjs'

export function Activity() {
  const [range, setRange] = useState({
    from: dayjs().subtract(7, 'day').toDate(),
    to: dayjs().toDate()
  })

  return (
    <RangeFlow
      defaultRange={{
        from: dayjs().subtract(90, 'day').toDate(),
        to: dayjs().toDate()
      }}
      defaultSelected={range}
      ranges={[
        { label: '7d', from: dayjs().subtract(6, 'day').toDate(), to: dayjs().toDate() },
        { label: '30d', from: dayjs().subtract(29, 'day').toDate(), to: dayjs().toDate() }
      ]}
      onChange={setRange}
    />
  )
}

Why rangeflow

A picker that actually moves.

Drag-native range slider

Built for mouse and touch. The whole window is grabbable, both handles snap with weight.

Quick-range tabs

Pre-defined windows like 7 / 30 / 90 days with an animated active pill.

Popover calendar

A real calendar one click away. Choose single or multi-month.

One-variable theming

Set --rangeflow-accent and the picker matches the rest of your design system.

Slot-based composition

Replace any visible part. Swap tabs, tickers, value labels, or the selected-date readout.

Imperative API

The useRangeflow() hook exposes updateRange / updateSelectedDates for forms, URLs, and external buttons.

Props

Every prop, at a glance.

Three required props get you a working picker. The rest narrow the surface: bounds, disabled ranges, presets, slots, imperative control.

PropTypeDefaultDescription
defaultRange*{ from: Date; to: Date }N/AThe outer window the slider spans. The user drags the selection inside this range.
defaultSelected*{ from: Date; to: Date }N/AThe initial selected range. Must fall inside defaultRange.
onChange*(date: { from: Date; to: Date }) => voidN/AFires whenever the user drags the slider or picks a date from the calendar.
rangesRangeListItem[]5 built-in presetsQuick range tabs shown above the slider. Clicking a tab resets the outer window to that preset.
duration{ min: number; max: number }N/AMin / max number of days the selection can span. Applied as soft clamps while dragging.
disabled{ before: Date } | { after: Date } | { before; after }N/ADisable dates outside a boundary. Applies to both slider and calendar.
calendarbooleantrueShow the popover calendar trigger next to the selected date label.
CalendarPropsDayPickerPropsN/AForwarded directly to the underlying react-day-picker.
SlotsSlotsN/AOverride individual parts (RangeTabs, DateTickers, SelectedDate, SliderValueLabel, DateLabelsTrack). See the Slots section.
apiRangeFlowApiN/AImperative handle from useRangeflow() for programmatic control. See the Hook section.
* required

Minimal example

Only three props are required. Everything else has a sensible default.

components/picker.tsx
<RangeFlow
  defaultRange={{ from: lastMonth, to: today }}
  defaultSelected={{ from: lastWeek, to: today }}
  onChange={setRange}
/>

Duration: Min / max days

Clamp how many days the selection can cover. Drag the slider. It won't shrink below min or grow beyond max.

14 AprToday
 
6 Days
 
Mar 06Mar 16Mar 26Apr 05Apr 15Apr 25May 05May 15May 25Jun 04

selected 7 days

duration.tsx
<RangeFlow
  defaultRange={window}
  defaultSelected={selected}
  duration={{ min: 3, max: 30 }}
  onChange={setRange}
/>

Disabled: Bound the selectable range

Block selection before / after a cutoff. Applies to both slider and calendar. Typical use: lock to the past for analytics.

 
4 Days
 
Mar 21Mar 28Apr 04Apr 10Apr 17Apr 24Apr 30May 07May 14May 20
disabled.tsx
// Lock to past. Common for analytics.
<RangeFlow
  defaultRange={window}
  defaultSelected={selected}
  disabled={{ after: new Date() }}
  onChange={setRange}
/>

Ranges: Quick preset tabs

Preset tabs above the slider. Clicking a tab re-fits the outer window and selects that range.

 
1 Day
 
Apr 07Apr 09Apr 11Apr 13Apr 15Apr 17Apr 19Apr 20
ranges.tsx
<RangeFlow
  defaultRange={window}
  defaultSelected={selected}
  ranges={[
    { label: '7d',  from: sub(today, 6, 'day'),  to: today },
    { label: '30d', from: sub(today, 29, 'day'), to: today },
    { label: '90d', from: sub(today, 89, 'day'), to: today }
  ]}
  onChange={setRange}
/>

Calendar: Toggle the popover picker

Hide the popover calendar when the slider is enough. This keeps the UI compact on dense dashboards.

 
6 Days
 
Mar 21Mar 28Apr 04Apr 10Apr 17Apr 24Apr 30May 07May 14May 20
calendar.tsx
<RangeFlow
  defaultRange={window}
  defaultSelected={selected}
  calendar={false}
  onChange={setRange}
/>

Hook

Drive it from outside with useRangeflow().

An imperative handle for when the picker needs to react to things it doesn't own. URL params, other filters, reset buttons, and keyboard shortcuts all fit here.

Mental model

  • Uncontrolled by default. The component owns its state; onChange tells you about changes.
  • The hook is the escape hatch. When external state needs to push into the picker, pass the api handle.
  • Don't reach for it first. If onChange covers your case, skip the hook.
wiring.tsx
import { RangeFlow, useRangeflow } from 'rangeflow'

export function Picker() {
  const api = useRangeflow()

  return (
    <>
      <button onClick={() => api.updateSelectedDates({ from, to })}>
        Last 7 days
      </button>
      <RangeFlow
        api={api}
        defaultRange={{ from, to }}
        defaultSelected={{ from, to }}
        onChange={setRange}
      />
    </>
  )
}
MethodSignatureDescription
updateRange(range: { from: Date; to: Date }) => voidReplace the outer window the slider spans. The current selection re-fits inside the new window. Does not call onChange.
updateSelectedDates(dates: { from: Date; to: Date }) => voidMove the slider thumb to a new selection inside the current window. Emits onChange with the new value.

Jump the selection with updateSelectedDates

Move the selection from outside the component. Common for quick-action buttons, keyboard shortcuts, or syncing with URL params.

13 AprToday
 
7 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20

selected: 13 Apr20 Apr

selection.tsx
const api = useRangeflow()

<button onClick={() =>
  api.updateSelectedDates({
    from: dayjs().subtract(6, 'day').toDate(),
    to: dayjs().toDate()
  })
}>
  Last 7 days
</button>

<RangeFlow api={api} defaultRange={window} defaultSelected={initial} onChange={setRange} />

Change the window with updateRange

Swap the outer window. The current selection re-fits inside. Use this for zoom / time-scale controls.

 
6 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20
range.tsx
const api = useRangeflow()

<button onClick={() =>
  api.updateRange({
    from: dayjs().subtract(89, 'day').toDate(),
    to: dayjs().toDate()
  })
}>
  Zoom to 90 days
</button>

<RangeFlow api={api} defaultRange={window} defaultSelected={initial} onChange={setRange} />

Slots

Swap any part. Keep the rest.

Each slot is a full replacement. Pass a React component and it renders in place of the built-in. Use the CSS tokens --rangeflow-* so custom content stays on-theme.

SlotPropsDescription
SelectedDate{ from: string; to: string }The formatted date label in the picker bar (left of the range tabs). Receives pre-formatted strings.
SliderValueLabel{ label: string }The floating pill above the slider thumb. Receives the default label (e.g. "7 Days").
DateTickersN/AThe vertical ticks rendered across the slider track. Replace with any visualization (heatmap, sparkline, etc).
DateLabelsTrackN/AThe date labels row beneath the slider. Replace to change formatting, density, or layout.
RangeTabsN/AThe preset tabs row in the picker bar. Full replacement. You own selection state if you override this.

SliderValueLabel

The pill above the slider thumb. Receives the default label. Turn it into a badge, icon, or anything else.

14 AprToday
 
📆6 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20
value-label.tsx
function CustomLabel({ label }: { label: string }) {
  const days = parseInt(label, 10) || 1
  return (
    <div className="flex items-center gap-1.5 rounded-full bg-[var(--rangeflow-accent-solid)] px-2.5 py-1 text-[11px] font-semibold text-[var(--rangeflow-accent-contrast)]">
      <span>{days === 1 ? '🌤️' : days < 7 ? '📆' : '📊'}</span>
      {label}
    </div>
  )
}

<RangeFlow
  defaultRange={window}
  defaultSelected={selected}
  Slots={{ SliderValueLabel: CustomLabel }}
  onChange={setRange}
/>

SelectedDate

The date display in the picker bar. Receives pre-formatted strings so you don't re-implement formatting.

14 AprToday
 
6 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20
selected-date.tsx
function CustomSelectedDate({ from, to }: { from: string; to: string }) {
  return (
    <div className="flex items-center gap-2 text-xs font-medium">
      <span className="rounded-md bg-[var(--rangeflow-hover-bg)] px-2 py-0.5 text-[var(--rangeflow-accent-text)]">{from}</span>
      <span>→</span>
      <span className="rounded-md bg-[var(--rangeflow-hover-bg)] px-2 py-0.5 text-[var(--rangeflow-accent-text)]">{to}</span>
    </div>
  )
}

<RangeFlow Slots={{ SelectedDate: CustomSelectedDate }}  />

DateTickers

The tick row on the slider track. Ignore it, turn it into a gradient, or render a heatmap of your data.

14 AprToday
 
6 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20
tickers.tsx
function HeatmapTickers() {
  return (
    <div className="flex w-full items-center gap-[2px]">
      {cells.map(i => (
        <div key={i} className="h-3 flex-1 rounded-[1px]"
          style={{ backgroundColor: colorFor(i) }}
        />
      ))}
    </div>
  )
}

<RangeFlow Slots={{ DateTickers: HeatmapTickers }}  />

DateLabelsTrack

The date labels under the slider. It is absolutely positioned. Keep `absolute top-10 left-0` on your replacement.

06 AprToday
 
14 Days
 
start···now
labels.tsx
function MinimalLabels() {
  return (
    <div className="absolute top-10 left-0 flex w-full justify-between px-2 text-[10px] tracking-[0.18em] uppercase text-[var(--rangeflow-text-faint)]">
      <span>start</span><span>·</span><span>·</span><span>·</span><span>now</span>
    </div>
  )
}

<RangeFlow Slots={{ DateLabelsTrack: MinimalLabels }}  />

Theming

One variable. The rest is derived.

Set --rangeflow-accent and every border, hover, range, and ring re-balances via color-mix(). Override individual tokens only when you want finer control.

globals.css
:root {
  --rangeflow-accent: #4f46e5;
}

/* Or scope to one instance */
.my-picker {
  --rangeflow-accent: #4f46e5;
}
dark-mode.tsx
{/* Any of these enables dark mode */}
<div className="dark"><RangeFlow {...props} /></div>
<div data-theme="dark"><RangeFlow {...props} /></div>

{/* Works with parents too. Tailwind's dark: prefix integrates out of the box. */}
<html className="dark">
  <body><RangeFlow {...props} /></body>
</html>

Live playground

Pick a color, toggle dark mode. The accent drives border, hover, range, ring, and ticker. Everything stays balanced.

accent
#4f46e5
 
6 Days
 
Mar 21Mar 26Mar 30Apr 03Apr 08Apr 12Apr 16Apr 20
<div style={{ '--rangeflow-accent': '#4f46e5' }}>
  <RangeFlow {...props} />
</div>
playground.tsx
<div
  data-theme={dark ? 'dark' : 'light'}
  style={{ '--rangeflow-accent': accent }}
>
  <RangeFlow {...props} />
</div>

Tokens

Set any of these on the picker root or any ancestor. Core tokens drive the system; derived tokens are available when you need to break out of the defaults.

Core (set these)
--rangeflow-accent#16433CBrand color. Drives most other tokens.
--rangeflow-surface#ffffff / #0a0f0cBackground of the picker (light / dark).
--rangeflow-foreground#0a0f0c / #ffffffText base color (light / dark).
--rangeflow-on-accentautoText rendered on solid accent (auto black/white).
--rangeflow-fontsystem stackFont family used inside the picker.
Derived (override only when needed)
--rangeflow-bg= surfaceInner background.
--rangeflow-bordermixDefault border.
--rangeflow-border-strongmixStronger border.
--rangeflow-shadow-colormixShadow tint.
--rangeflow-textmixMain text.
--rangeflow-text-mutedmixSecondary text.
--rangeflow-text-subtlemixLabels.
--rangeflow-text-faintmixSeparators and faint labels.
--rangeflow-text-disabledmixDisabled items.
--rangeflow-hover-bgmixHover background.
--rangeflow-range-bgmixBackground of the picked range.
--rangeflow-active-bgmixActive tab pill background.
--rangeflow-accent-solid= accentSolid accent fills.
--rangeflow-accent-solid-hovermixHover for solid accent.
--rangeflow-accent-contrast= on-accentText on solid accent.
--rangeflow-accent-textmixTinted accent text (e.g. selected date).
--rangeflow-ring= accentFocus ring.
--rangeflow-separatormixSeparator lines.
--rangeflow-separator-active= accentSeparator on active state.
--rangeflow-tickermixTick marks on the slider.
--rangeflow-today= accent-textThe "today" marker on the calendar.

Class names

Style without tokens when you need to.

Every scoped class starts with .rangeflow-. Prefer CSS variables first. Reach for classes only when you need structural changes (spacing, sizing, layout).

Roots
.rangeflow-date-pickerRoot of the picker. All scoped styles live under here.
.rangeflow-date-picker-portalRoot of any portalled part (calendar popover).
Structure
.rangeflow-rootOuter wrapper (inside the root).
.rangeflow-headerTop bar holding the date and tabs.
.rangeflow-bodyBottom area with the slider.
.rangeflow-sliderSlider track container.
Parts
.rangeflow-tabsRange tabs container.
.rangeflow-tabOne tab button.
.rangeflow-tab-indicatorAnimated active tab pill.
.rangeflow-selected-dateThe current selection label.

Overriding safely

Always scope your selectors to .rangeflow-date-picker so nothing leaks into the rest of your app.

overrides.css
/* Targets only RangeFlow, not your app */
.rangeflow-date-picker .rangeflow-tab {
  border-radius: 999px;
  font-weight: 600;
}

.rangeflow-date-picker .rangeflow-selected-date {
  letter-spacing: 0.02em;
}

Ship a date picker your users enjoy using.