Skip to content

leslieduan/date-slider-lib

Repository files navigation

DateSlider

A powerful, fully customizable React date slider component with range, point, and combined selection modes.

npm version Storybook License: MIT

Features

  • 3 Selection Modes: Point, Range, and Combined
  • 4 Time Units: Hour, Day, Month, and Year granularity
  • Step Navigation: Configurable step amounts for keyboard and button navigation (static or dynamic)
  • Fully Customizable: Style with Tailwind CSS classes and custom scale type resolvers
  • TypeScript: Complete type safety
  • Accessible: WCAG compliant with keyboard navigation
  • Mobile-Optimized: Auto-adapts with persistent labels on mobile
  • Optional UI Components: Time display, unit selector, and date labels with defaults
  • High Performance: Automatic virtualization for large date ranges
  • Imperative API: Programmatic control with setDateTime, moveByStep, and focusHandle

Performance

DateSlider automatically optimizes rendering for large date ranges using virtualization. Only visible elements are rendered in the DOM, dramatically improving performance with no configuration required.

Performance Example: A 10-year date range with daily granularity:

  • Without virtualization: ~7,300 DOM elements
  • With virtualization: ~200 DOM elements (97% reduction)

How it works:

  • Virtualization activates automatically when scrollable={true} and the track width exceeds the viewport
  • Only scale marks and time labels in the visible area (plus a buffer zone) are rendered
  • As you scroll, elements smoothly appear and disappear
  • Completely transparent to users - no API changes or configuration needed

Example of a large date range:

<DateSlider
  mode="range"
  value={{
    start: new Date('2020-01-01'),
    end: new Date('2022-12-31'),
  }}
  min={new Date('2015-01-01')}
  max={new Date('2025-12-31')}  // 10-year range
  initialTimeUnit="day"
  behavior={{ scrollable: true }}  // Virtualization activates automatically
/>

Note: Non-scrollable sliders and small date ranges that fit in the viewport render all elements normally. Virtualization only activates when needed for performance.

Installation

npm install date-slider-lib

Setup

1. Import CSS (required):

import 'date-slider-lib/style.css';

2. Configure Tailwind (if using):

// tailwind.config.js
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./node_modules/date-slider-lib/dist/**/*.{js,mjs,cjs}",
  ],
}

Quick Start

import { DateSlider } from 'date-slider-lib';

function App() {
  const [value, setValue] = useState({ point: new Date() });

  return (
    <DateSlider
      mode="point"
      value={value}
      onChange={setValue}
      min={new Date('2024-01-01')}
      max={new Date('2024-12-31')}
      initialTimeUnit="day"
    />
  );
}

Modes

Point Mode

<DateSlider
  mode="point"
  value={{ point: new Date('2024-06-15') }}
  onChange={setValue}
  min={new Date('2024-01-01')}
  max={new Date('2024-12-31')}
  initialTimeUnit="day"
/>

Range Mode

<DateSlider
  mode="range"
  value={{
    start: new Date('2024-03-01'),
    end: new Date('2024-09-01')
  }}
  onChange={setValue}
  min={new Date('2024-01-01')}
  max={new Date('2024-12-31')}
  initialTimeUnit="month"
/>

Combined Mode

<DateSlider
  mode="combined"
  value={{
    point: new Date('2024-06-15'),
    start: new Date('2024-03-01'),
    end: new Date('2024-09-01')
  }}
  onChange={setValue}
  min={new Date('2024-01-01')}
  max={new Date('2024-12-31')}
  initialTimeUnit="month"
/>

Customization

Styling

<DateSlider
  mode="range"
  value={{ start: new Date('2024-03-01'), end: new Date('2024-09-01') }}
  classNames={{
    trackActive: 'bg-blue-500/30',
    track: 'bg-gray-300',
    handle: 'bg-blue-600 border-2 border-white shadow-lg',
    scaleMarkMajor: 'bg-gray-600',
    scaleLabel: 'text-gray-700',
  }}
/>

Custom Icons (Optional)

Icons are optional. Defaults are provided.

import { MyIcon } from './icons';

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  icons={{ point: <MyIcon /> }}
/>

<DateSlider
  mode="range"
  value={{ start: new Date(), end: new Date() }}
  icons={{ range: <MyRangeIcon /> }}
/>

<DateSlider
  mode="combined"
  value={{ point: new Date(), start: new Date(), end: new Date() }}
  icons={{
    point: <MyPointIcon />,
    range: <MyRangeIcon />
  }}
/>

UI Components

Enable optional UI components. Default renderers are provided.

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  layout={{
    dateLabelEnabled: true,           // Show date labels on handles
    selectionPanelEnabled: true,          // Show time display with navigation
    timeUnitSelectionEnabled: true,    // Show day/month/year selector
  }}
  // No renderProps needed - defaults are provided!
/>

Customize renderers (optional):

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  layout={{
    dateLabelEnabled: true,
    selectionPanelEnabled: true,
  }}
  renderProps={{
    renderDateLabel: ({ label }) => <span className="...">{label}</span>,
    renderSelectionPanel: ({ dateLabel, toNextDate, toPrevDate }) => (
      <div>
        <button onClick={toPrevDate}></button>
        <span>{dateLabel}</span>
        <button onClick={toNextDate}></button>
      </div>
    ),
  }}
/>

Date Formatting

Customize date formats using standard dayjs format tokens. Specify formats separately for scale marks and handle labels:

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  dateFormat={{
    scale: ({ date, unit }) => {
      const day = date.getUTCDate();
      if (day === 1) return 'MMM';      // "Jun" on first day
      return 'DD';                       // "15" on other days
    },
    label: ({ date, unit }) => 'DD-MMM-YYYY',  // "15-Jun-2024"
  }}
/>

Format function signature:

type DateFormatFn = (params: {
  date: Date;        // The date to format
  unit?: TimeUnit;   // Current time unit (hour/day/month/year)
}) => string;

Common format tokens: YYYY, MM, DD, MMM, MMMM, HH, mm, ddd, dddd

Full token list: dayjs format documentation

Separator examples:

You can specify scale only, label only, or both:

// Same format for both scale and labels
dateFormat={{ scale: ({ date }) => 'YYYY-MM-DD' }}

// Different formats
dateFormat={{
  scale: ({ date }) => 'DD',
  label: ({ date }) => 'DD-MMM-YYYY'
}}

Locale Support

Format dates in different languages using dayjs locales. Import the locale you need and pass the locale code:

import 'dayjs/locale/fr';  // French
import 'dayjs/locale/de';  // German
import 'dayjs/locale/ja';  // Japanese
import 'dayjs/locale/es';  // Spanish

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  locale="fr"  // French locale
  dateFormat={{
    scale: ({ date }) => 'DD MMM',
    label: ({ date }) => 'dddd, DD MMMM YYYY'  // "lundi, 15 juin 2024"
  }}
/>

Available locales: dayjs locale list

Note: Import locales at your app's entry point or before using the component. The default locale is 'en' (English).

Time Units

DateSlider supports four time unit granularities for different use cases:

Hour

Perfect for detailed timeline views, event scheduling, or time tracking applications:

<DateSlider
  mode="point"
  value={{ point: new Date('2024-12-07T12:00:00Z') }}
  min={new Date('2024-12-07T00:00:00Z')}
  max={new Date('2024-12-08T23:59:59Z')}
  initialTimeUnit="hour"
  dateFormat={{
    scale: ({ date }) => {
      const hour = date.getUTCHours();
      if (hour === 0) return 'DD HH:mm';  // Midnight: show date
      return 'HH:mm';                      // Other hours: show time
    },
    label: ({ date }) => 'DD MMM YYYY HH:mm'
  }}
/>

Day

Standard daily granularity for most date selection use cases:

<DateSlider
  mode="range"
  initialTimeUnit="day"
  // ... other props
/>

Month

Monthly granularity for longer-term planning:

<DateSlider
  mode="range"
  initialTimeUnit="month"
  // ... other props
/>

Year

Yearly granularity for historical or long-term date ranges:

<DateSlider
  mode="range"
  initialTimeUnit="year"
  // ... other props
/>

Custom Scale Type Resolver

Control the visual hierarchy of scale marks (short/medium/long) with a custom resolver function. This allows you to customize which dates get emphasized with different tick mark heights.

Default behavior (when no custom resolver provided):

  • Hour: long=year start, medium=month start, short=each hour
  • Day: long=month start, medium=Monday, short=each day
  • Month: long=year start, medium=quarter start, short=each month
  • Year: long=decade start, medium=5-year mark, short=each year

Partial resolver example - Only customize hour timeUnit, use defaults for others:

<DateSlider
  mode="point"
  initialTimeUnit="hour"
  scaleTypeResolver={(date, timeUnit) => {
    // Only handle hour timeUnit
    if (timeUnit === 'hour') {
      const hour = date.getUTCHours();
      if (hour % 6 === 0) return 'long';    // Every 6 hours
      if (hour % 3 === 0) return 'medium';  // Every 3 hours
      return 'short';                        // Other hours
    }
    // Return undefined to use default logic for day/month/year
    return undefined;
  }}
/>

The resolver function:

  • Receives: (date: Date, timeUnit: TimeUnit)
  • Returns: 'long' | 'medium' | 'short' | undefined
    • 'long': Tallest tick marks (major divisions)
    • 'medium': Medium tick marks (intermediate divisions)
    • 'short': Shortest tick marks (minor divisions)
    • undefined: Fall back to default logic for this timeUnit

Tip: Return undefined for timeUnits you don't want to customize. This allows partial customization while using sensible defaults for the rest!

Label Behavior

<DateSlider
  mode="combined"
  value={{ point: new Date(), start: new Date(), end: new Date() }}
  behavior={{
    handleLabelPersistent: true,           // All handles: always visible
    pointHandleLabelPersistent: true,      // Point only: always visible
    rangeHandleLabelPersistent: false,     // Range only: always visible
    trackHoverDateLabelDisabled: false,    // Enable/disable track hover label
    trackHoverCursorLineDisabled: false,   // Enable/disable cursor line
  }}
/>

Note: On mobile, labels are automatically persistent for better usability.

Step Navigation

Configure how the slider navigates when using keyboard arrow keys or SelectionPanel buttons. You can use a static step amount or a dynamic callback function that adapts to context.

Static Step

Navigate by a fixed amount:

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  behavior={{
    step: { amount: 7, unit: 'day' }  // Move 7 days at a time
  }}
/>

Dynamic Step (Callback)

Adapt step amount based on current zoom level, date, or handle:

<DateSlider
  mode="point"
  value={{ point: new Date() }}
  layout={{
    timeUnitSelectionEnabled: true,  // Allow switching between hour/day/month/year
  }}
  behavior={{
    // Step adapts to current timeUnit
    step: ({ unit }) => {
      if (unit === 'hour') return { amount: 6, unit: 'hour' };
      if (unit === 'day') return { amount: 7, unit: 'day' };
      if (unit === 'month') return { amount: 3, unit: 'month' };
      return { amount: 1, unit };
    }
  }}
/>

Step function context:

type StepFn = (context: {
  date: Date;     // Current date at handle position
  unit: TimeUnit;        // Current zoom level (hour/day/month/year)
  handle: DragHandle;    // Which handle is being moved ('start'/'end'/'point')
}) => Step;

Use cases:

  • Adaptive navigation: Different steps for different zoom levels
  • Date-aware stepping: Jump to first of month, quarters, etc.
  • Handle-specific steps: Different steps for range start vs end

Default behavior: If no step is configured, the slider moves by 1 unit of the current timeUnit.

Layout & Behavior

<DateSlider
  mode="range"
  value={{ start: new Date(), end: new Date() }}
  layout={{
    width: 800,                     // or 'fill'
    height: 100,
    trackPaddingX: 40,
    showEndLabel: true,
    minGapScaleUnits: 50,
    dateLabelDistanceOverHandle: 35,
    scaleUnitConfig: {
      gap: 100,
      width: { short: 1, medium: 2, long: 2 },
      height: { short: 18, medium: 36, long: 60 },
    },
  }}
  behavior={{
    scrollable: true,
    freeSelectionOnTrackClick: false,
    sliderAutoScrollToPointHandleVisibleEnabled: true,
  }}
/>

Imperative API

Control the slider programmatically using a ref. Three methods are available:

import { useRef } from 'react';
import type { SliderExposedMethod } from 'date-slider-lib';

function App() {
  const sliderRef = useRef<SliderExposedMethod>(null);

  const setToToday = () => {
    // Absolute positioning: set to a specific date
    sliderRef.current?.setDateTime(new Date(), 'point');
  };

  const moveForward = () => {
    // Relative positioning: move by configured step
    sliderRef.current?.moveByStep('forward', 'point');
  };

  const moveBackward = () => {
    // Relative positioning: move by configured step
    sliderRef.current?.moveByStep('backward', 'point');
  };

  const focusHandle = () => {
    // Focus management: set keyboard focus to handle
    sliderRef.current?.focusHandle('point');
  };

  return (
    <>
      <DateSlider
        mode="point"
        value={{ point: new Date() }}
        imperativeRef={sliderRef}
        behavior={{
          step: { amount: 7, unit: 'day' }  // moveByStep will use this
        }}
      />
      <button onClick={setToToday}>Set to Today</button>
      <button onClick={moveBackward}>← Previous Week</button>
      <button onClick={moveForward}>Next Week →</button>
      <button onClick={focusHandle}>Focus Handle</button>
    </>
  );
}

API Methods:

  • setDateTime(date: Date, target?: DragHandle): Set handle to a specific date

    • date: UTC Date to set
    • target: Optional - which handle to move ('start', 'end', 'point'). Auto-detects if not provided.
  • moveByStep(direction: 'forward' | 'backward', target?: DragHandle): Move handle by configured step amount

    • direction: 'forward' or 'backward'
    • target: Optional - which handle to move. Defaults based on mode.
    • Uses the step configuration from behavior.step
  • focusHandle(handleType: DragHandle): Set keyboard focus to a handle

    • handleType: Which handle to focus ('start', 'end', 'point')

API Reference

Core Props

Prop Type Required Description
mode 'point' | 'range' | 'combined' Yes Selection mode
value PointValue | RangeValue | CombinedValue Yes Current selection (UTC dates)
onChange (value) => void Yes Selection change callback
min Date No Minimum date (UTC)
max Date No Maximum date (UTC)
initialTimeUnit 'hour' | 'day' | 'month' | 'year' No Initial time unit
scaleTypeResolver function No Custom function to determine scale types
icons object No Custom icons (optional, defaults provided)
classNames object No Tailwind classes for styling
behavior object No Interaction behavior options
layout object No Size and layout configuration
renderProps object No Custom render functions (optional)
dateFormat object No Custom date format for scale and labels { scale?, label? }
locale string No Date locale for formatting (default: 'en')
imperativeRef Ref No Imperative API reference

Value Types

type PointValue = { point: Date };
type RangeValue = { start: Date; end: Date };
type CombinedValue = { point: Date; start: Date; end: Date };

BehaviorConfig

{
  // Navigation
  scrollable?: boolean;
  freeSelectionOnTrackClick?: boolean;
  sliderAutoScrollToPointHandleVisibleEnabled?: boolean;
  step?: Step | StepFn;  // Static step or dynamic callback

  // Label behavior
  handleLabelPersistent?: boolean;
  handleLabelDisabled?: boolean;
  pointHandleLabelPersistent?: boolean;
  pointHandleLabelDisabled?: boolean;
  rangeHandleLabelPersistent?: boolean;
  rangeHandleLabelDisabled?: boolean;

  // Track hover behavior
  trackHoverDateLabelDisabled?: boolean;
  trackHoverCursorLineDisabled?: boolean;
}

// Step types
type Step = {
  amount: number;
  unit: 'hour' | 'day' | 'month' | 'year';
};

type StepFn = (context: {
  date: Date;
  unit: TimeUnit;
  handle: DragHandle;
}) => Step;

LayoutConfig

{
  width: 'fill' | number;
  height?: number;
  trackPaddingX?: number;
  showEndLabel?: boolean;
  minGapScaleUnits?: number;
  scaleUnitConfig?: object;
  dateLabelDistanceOverHandle?: number;

  // Component toggles (default: false)
  selectionPanelEnabled?: boolean;
  timeUnitSelectionEnabled?: boolean;
  dateLabelEnabled?: boolean;
}

Mobile Behavior

On small screens:

  • Handle labels become persistent (always visible)
  • Touch-optimized interactions
  • Responsive sizing

Test mobile behavior by resizing your browser to mobile viewport.

Accessibility

  • Keyboard navigation:
    • Arrow keys: Move by configured step amount
    • Home: Jump to minimum date
    • End: Jump to maximum date
    • Tab: Move between handles
  • ARIA labels for screen readers
  • Visible focus indicators
  • Touch support with mobile optimizations

License

MIT


Made with ❤️ by Leslie Duan

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Packages

No packages published