ng-date-hour-range-selector

May 22, 2026 · View on GitHub

Live Demo npm

A flexible Angular date / date-time range selector built on Angular CDK Overlay. Supports predefined range shortcuts, time picking, localization, and full CSS customization — with zero third-party date-library dependency.

→ Live Demo


Features

  • Date and time range selection, or date-only mode
  • 12-hour (AM/PM) and 24-hour time formats
  • Configurable minute step
  • Optional manual time inputs for direct hour/minute editing
  • Sidebar with predefined range shortcuts (Today, Yesterday, This/Last Week…)
  • Configurable calendar icon position (left, right, or hidden)
  • Optional reset button in the sidebar
  • Works as a ControlValueAccessor — drop into any reactive or template-driven form
  • Fully localizable via the PICKER_LOCALE injection token
  • Pre-select a range on load via the initialRange input
  • nextRange() / previousRange() / setRange() public API methods
  • No third-party date library required
  • Built on Angular CDK Overlay
  • Standalone components — no NgModule needed
  • A directive variant (drsDateRangePicker) to attach the picker to any <input>
  • Accessible: keyboard navigation, ARIA attributes, meets WCAG AA
  • Dark theme included; fully themeable via CSS custom properties

Requirements

DependencyVersion
Angular>=19.0.0
@angular/cdk>=19.0.0

Installation

npm install ng-date-hour-range-selector @angular/cdk

Import the built-in dark theme once in your global styles:

@use 'ng-date-hour-range-selector/styles/theme';

Add provideAnimationsAsync() to your application config:

import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
  providers: [provideAnimationsAsync()],
};

Quick start

Component (<drs-date-range-picker>)

import { DateRange, DateRangePickerComponent } from 'ng-date-hour-range-selector';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
  imports: [ReactiveFormsModule, DateRangePickerComponent],
  template: `
    <drs-date-range-picker
      [formControl]="rangeControl"
      (rangeChange)="onRangeChange($event)"
      ariaLabel="Select date range"
    />
  `,
})
export class MyComponent {
  readonly rangeControl = new FormControl<DateRange | null>(null);

  onRangeChange(range: DateRange | null): void {
    console.log(range?.start, range?.end);
  }
}

Directive ([drsDateRangePicker])

Attach the picker to any <input> element:

import { DateRangePickerDirective } from 'ng-date-hour-range-selector';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
  imports: [ReactiveFormsModule, DateRangePickerDirective],
  template: `
    <input
      drsDateRangePicker
      [formControl]="rangeControl"
      (rangeChange)="onRangeChange($event)"
      ariaLabel="Select date range"
    />
  `,
})
export class MyComponent {
  readonly rangeControl = new FormControl<DateRange | null>(null);

  onRangeChange(range: DateRange | null): void {
    console.log(range?.start, range?.end);
  }
}

Component API — <drs-date-range-picker>

Inputs

InputTypeDefaultDescription
showTimebooleantrueShow the time-picker section
timeFormat'12h' | '24h''24h'12-hour (AM/PM) or 24-hour format
minuteStepnumber1Minute increment step
allowManualTimeInputbooleanfalseEnable editable hour/minute text inputs in the time picker
weekStartsOn0 | 11First day of week — 0 Sunday, 1 Monday
predefinedRangesPredefinedRange[]built-inSidebar shortcut definitions
minDateDateMinimum selectable date (inclusive)
maxDateDateMaximum selectable date (inclusive)
positionConnectedPosition[]bottom-startCDK Overlay connected positions
showResetButtonbooleantrueShow or hide the reset button in the sidebar
calendarIcon'left' | 'right' | 'hidden''right'Position of the calendar icon in the trigger button, or hide it
showApplyButtonbooleanfalseShow an Apply button inside the overlay that closes it when clicked
closeOnSelectbooleantrueAutomatically close the overlay after a complete range is selected or pre-defined
rangeMatchMode'day' | 'exact''day'How selected ranges are matched to predefined labels — 'day' ignores time, 'exact' requires identical timestamps
emitOn'change' | 'close''change'Controls when rangeChange is emitted. 'change' — emit immediately on every date/time selection (default). 'close' — defer emission until the overlay is closed or Apply is clicked; reset always emits immediately.
initialRangeDateRange | PredefinedRangeRange or predefined-range factory to pre-select on component load
ariaLabelstring'Select date range'Accessible label for the trigger button

Output

OutputPayloadDescription
rangeChangeDateRange | nullEmitted when a complete range is committed or cleared

Public methods

MethodDescription
nextRange()Advance the current range forward by its own duration (e.g. Mon–Sun → next Mon–Sun)
previousRange()Rewind the current range backward by its own duration
setRange(range, emitEvent?)Programmatically set DateRange | null; pass emitEvent: false to suppress rangeChange and CVA onChange

ControlValueAccessor

DateRangePickerComponent implements ControlValueAccessor, so it works with both [formControl] and [(ngModel)]:

<!-- Reactive forms -->
<drs-date-range-picker [formControl]="rangeControl" />

<!-- Template-driven -->
<drs-date-range-picker [(ngModel)]="range" />

Directive API — [drsDateRangePicker]

The directive exposes the same inputs and output as the component, except calendarIcon (which is specific to the component's trigger button).

<input drsDateRangePicker [formControl]="ctrl" [showTime]="false" />

Models

interface DateRange {
  start: Date;
  end: Date;
}

interface PredefinedRange {
  /** Label shown in the sidebar */
  label: string;
  /** Factory function — called on each click to produce a fresh range */
  range: () => DateRange;
}

Global configuration — PICKER_CONFIG

Override defaults for every picker in your application (or a specific feature):

import { PICKER_CONFIG } from 'ng-date-hour-range-selector';

// app.config.ts
providers: [
  {
    provide: PICKER_CONFIG,
    useValue: { showTime: false, timeFormat: '12h', weekStartsOn: 0 },
  },
];

Individual component/directive inputs always take precedence over the global config.

PickerConfig interface

PropertyTypeDefaultDescription
showTimebooleantrueShow the time-picker section
timeFormat'12h' | '24h''24h'Hour format
minuteStepnumber1Minute increment step
allowManualTimeInputbooleanfalseEnable editable hour/minute text inputs in the time picker
weekStartsOn0 | 11First day of week
predefinedRangesPredefinedRange[]built-inOverride all shortcuts globally
minDateDateGlobal minimum date
maxDateDateGlobal maximum date
positionConnectedPosition[]bottom-startCDK overlay positions
showResetButtonbooleantrueShow or hide the reset button
calendarIcon'left' | 'right' | 'hidden''right'Calendar icon position in the trigger button
showApplyButtonbooleanfalseShow an Apply button inside the overlay
closeOnSelectbooleantrueAutomatically close the overlay after a complete range is selected or pre-defined
rangeMatchMode'day' | 'exact''day'How selected ranges are matched to predefined labels — 'day' ignores time, 'exact' requires identical timestamps
emitOn'change' | 'close''change'Controls when rangeChange is emitted. 'change' — emit immediately on every date/time selection. 'close' — defer emission until the overlay is closed or Apply is clicked; reset always emits immediately.

Localization — PICKER_LOCALE

Provide a PickerLocale object to translate every visible string:

import { PICKER_LOCALE, PickerLocale } from 'ng-date-hour-range-selector';

const ptBrLocale: PickerLocale = {
  daysOfWeek: ['Do', 'Se', 'Te', 'Qu', 'Qi', 'Se', 'Sa'],
  monthNames: [
    'Janeiro',
    'Fevereiro',
    'Março',
    'Abril',
    'Maio',
    'Junho',
    'Julho',
    'Agosto',
    'Setembro',
    'Outubro',
    'Novembro',
    'Dezembro',
  ],
  am: 'AM',
  pm: 'PM',
  startTime: 'Início:',
  endTime: 'Fim:',
  reset: 'Limpar',
  apply: 'Aplicar',
  placeholder: 'Selecione um período',
  formatRange: (start, end) =>
    `${start.toLocaleDateString('pt-BR')} – ${end.toLocaleDateString('pt-BR')}`,
  formatRangeWithTime: (start, end) => {
    const fmt = (d: Date) =>
      `${d.toLocaleDateString('pt-BR')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
    return `${fmt(start)} – ${fmt(end)}`;
  },
};

providers: [{ provide: PICKER_LOCALE, useValue: ptBrLocale }];

formatRangeWithTime is optional. When showTime is true and it is provided, the trigger will include times in the display value. Falls back to formatRange if omitted.

PickerLocale interface

PropertyTypeDescription
daysOfWeek[string × 7]Abbreviated day labels — Sunday first
monthNames[string × 12]Full month names — January first
amstringAM toggle label
pmstringPM toggle label
startTimestringLabel above the start time picker
endTimestringLabel above the end time picker
resetstringReset/clear button label
applystringApply button label (used when showApplyButton is true)
placeholderstring?Trigger placeholder when no range is selected
formatRange(start: Date, end: Date) => stringFormats the selected range for display in the trigger
formatRangeWithTime(start: Date, end: Date) => stringFormats the range including time; falls back to formatRange if omitted

Predefined ranges

The sidebar shows these built-in shortcuts by default:

  • Today
  • Yesterday
  • This week / Last week
  • This month / Last month
  • This quarter / Last quarter

Replace them per-picker via the predefinedRanges input, or globally via PICKER_CONFIG:

import { PredefinedRange } from 'ng-date-hour-range-selector';

const customRanges: PredefinedRange[] = [
  {
    label: 'Last 7 days',
    range: () => {
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 6);
      start.setHours(0, 0, 0, 0);
      end.setHours(23, 59, 59, 0);
      return { start, end };
    },
  },
  {
    label: 'Last 30 days',
    range: () => {
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      start.setHours(0, 0, 0, 0);
      end.setHours(23, 59, 59, 0);
      return { start, end };
    },
  },
];
<drs-date-range-picker [predefinedRanges]="customRanges" />

Examples

Date-only picker

<drs-date-range-picker [showTime]="false" />

12-hour format, Sunday start

<drs-date-range-picker timeFormat="12h" [weekStartsOn]="0" />

Calendar icon on the left, no reset button

<drs-date-range-picker calendarIcon="left" [showResetButton]="false" />

Pre-selected range on load

readonly initialRange: PredefinedRange = {
  label: 'Last 7 days',
  range: () => {
    const end = new Date();
    const start = new Date();
    start.setDate(start.getDate() - 6);
    return { start, end };
  },
};
<drs-date-range-picker [initialRange]="initialRange" />
private picker = viewChild(DateRangePickerComponent);

next(): void { this.picker()?.nextRange(); }
prev(): void { this.picker()?.previousRange(); }

Directive on a plain input

<input drsDateRangePicker [formControl]="ctrl" [showTime]="false" />

Theming — CSS custom properties

Import the built-in dark theme and override variables at :root or on specific elements:

@use 'ng-date-hour-range-selector/styles/theme';

// Global accent colour
:root {
  --drs-primary: #3b82f6;
}

// Light theme override
drs-date-range-picker.light {
  --drs-bg: #ffffff;
  --drs-text: #111111;
  --drs-border: rgba(0, 0, 0, 0.12);
  --drs-hover: rgba(0, 0, 0, 0.06);
  --drs-range-bg: rgba(59, 130, 246, 0.12);
}

You can also use the style attribute inline:

<drs-date-range-picker style="--drs-primary: #8b5cf6;" />

Full variable reference

VariableDescriptionDefault
--drs-radiusOverlay panel border radius10px
--drs-radius-smButton border radius5px
--drs-sidebar-widthPredefined-ranges sidebar width148px
--drs-overlay-zz-index of the overlay panel1000
--drs-shadowOverlay panel box shadowdark shadow
--drs-bgOverlay / modal background#1e1f22
--drs-trigger-bgTrigger button background--drs-bg
--drs-primaryAccent / highlight colour#f97316
--drs-primary-fgForeground on accent colour#ffffff
--drs-textPrimary text colour#f1f1f1
--drs-text-mutedDimmed / secondary text35% opacity
--drs-borderBorder and divider colour8% white
--drs-hoverHover background7% white
--drs-range-bgIn-range day backgroundorange 14%
--drs-time-bgTime-picker box background5% white
--drs-font-familyFont familyinherit
--drs-font-sizeBase font size0.875rem
--drs-header-font-sizeMonth / year header size0.9375rem
--drs-header-font-weightMonth / year header weight700
--drs-weekday-font-sizeDay-of-week label size0.6875rem
--drs-day-font-sizeDay number size0.875rem
--drs-sidebar-font-sizePredefined-range label size0.875rem
--drs-time-font-sizeHour / minute number size1.375rem
--drs-ampm-font-sizeAM/PM toggle size0.9375rem
--drs-label-font-size"Start time:" / "End time:" label0.8125rem
--drs-trigger-font-sizeTrigger button text size0.875rem
--drs-apply-font-sizeApply button text size0.875rem

Exported API surface

// Components & Directive
export { DateRangePickerComponent } from 'ng-date-hour-range-selector';
export { DateRangePickerDirective } from 'ng-date-hour-range-selector';
export { DateRangePickerPanelComponent } from 'ng-date-hour-range-selector';
export { CalendarComponent } from 'ng-date-hour-range-selector';
export { TimePickerComponent } from 'ng-date-hour-range-selector';
export { PredefinedRangesComponent } from 'ng-date-hour-range-selector';

// Models
export type { DateRange, PredefinedRange } from 'ng-date-hour-range-selector';
export type { PickerConfig } from 'ng-date-hour-range-selector';
export type { PickerLocale } from 'ng-date-hour-range-selector';
export type { TimeValue } from 'ng-date-hour-range-selector';
export type { CalendarCell } from 'ng-date-hour-range-selector';
export type { ResolvedPickerConfig } from 'ng-date-hour-range-selector';

// Tokens & defaults
export { PICKER_CONFIG, DEFAULT_PICKER_CONFIG } from 'ng-date-hour-range-selector';
export { PICKER_LOCALE, DEFAULT_PICKER_LOCALE } from 'ng-date-hour-range-selector';

// Service
export { DateUtilsService } from 'ng-date-hour-range-selector';

Development

# Install dependencies
npm install

# Start the demo app at http://localhost:4200
npm start

# Build the library
npm run build:lib

# Build library + demo
npm run build

# Run unit tests
npm test

License

MIT