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.
- 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_LOCALEinjection token - Pre-select a range on load via the
initialRangeinput 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
| Dependency | Version |
|---|---|
| Angular | >=19.0.0 |
@angular/cdk |
>=19.0.0 |
npm install ng-date-hour-range-selector @angular/cdkImport 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()],
};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);
}
}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);
}
}| Input | Type | Default | Description |
|---|---|---|---|
showTime |
boolean |
true |
Show the time-picker section |
timeFormat |
'12h' | '24h' |
'24h' |
12-hour (AM/PM) or 24-hour format |
minuteStep |
number |
1 |
Minute increment step |
allowManualTimeInput |
boolean |
false |
Enable editable hour/minute text inputs in the time picker |
weekStartsOn |
0 | 1 |
1 |
First day of week — 0 Sunday, 1 Monday |
predefinedRanges |
PredefinedRange[] |
built-in | Sidebar shortcut definitions |
minDate |
Date |
— | Minimum selectable date (inclusive) |
maxDate |
Date |
— | Maximum selectable date (inclusive) |
position |
ConnectedPosition[] |
bottom-start | CDK Overlay connected positions |
showResetButton |
boolean |
true |
Show 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 |
showApplyButton |
boolean |
false |
Show an Apply button inside the overlay that closes it when clicked |
closeOnSelect |
boolean |
true |
Automatically 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. |
initialRange |
DateRange | PredefinedRange |
— | Range or predefined-range factory to pre-select on component load |
ariaLabel |
string |
'Select date range' |
Accessible label for the trigger button |
| Output | Payload | Description |
|---|---|---|
rangeChange |
DateRange | null |
Emitted when a complete range is committed or cleared |
| Method | Description |
|---|---|
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 |
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" />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" />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;
}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.
| Property | Type | Default | Description |
|---|---|---|---|
showTime |
boolean |
true |
Show the time-picker section |
timeFormat |
'12h' | '24h' |
'24h' |
Hour format |
minuteStep |
number |
1 |
Minute increment step |
allowManualTimeInput |
boolean |
false |
Enable editable hour/minute text inputs in the time picker |
weekStartsOn |
0 | 1 |
1 |
First day of week |
predefinedRanges |
PredefinedRange[] |
built-in | Override all shortcuts globally |
minDate |
Date |
— | Global minimum date |
maxDate |
Date |
— | Global maximum date |
position |
ConnectedPosition[] |
bottom-start | CDK overlay positions |
showResetButton |
boolean |
true |
Show or hide the reset button |
calendarIcon |
'left' | 'right' | 'hidden' |
'right' |
Calendar icon position in the trigger button |
showApplyButton |
boolean |
false |
Show an Apply button inside the overlay |
closeOnSelect |
boolean |
true |
Automatically 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. |
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 }];
formatRangeWithTimeis optional. WhenshowTimeistrueand it is provided, the trigger will include times in the display value. Falls back toformatRangeif omitted.
| Property | Type | Description |
|---|---|---|
daysOfWeek |
[string × 7] |
Abbreviated day labels — Sunday first |
monthNames |
[string × 12] |
Full month names — January first |
am |
string |
AM toggle label |
pm |
string |
PM toggle label |
startTime |
string |
Label above the start time picker |
endTime |
string |
Label above the end time picker |
reset |
string |
Reset/clear button label |
apply |
string |
Apply button label (used when showApplyButton is true) |
placeholder |
string? |
Trigger placeholder when no range is selected |
formatRange |
(start: Date, end: Date) => string |
Formats the selected range for display in the trigger |
formatRangeWithTime |
(start: Date, end: Date) => string |
Formats the range including time; falls back to formatRange if omitted |
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" /><drs-date-range-picker [showTime]="false" /><drs-date-range-picker timeFormat="12h" [weekStartsOn]="0" /><drs-date-range-picker calendarIcon="left" [showResetButton]="false" />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(); }<input drsDateRangePicker [formControl]="ctrl" [showTime]="false" />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;" />| Variable | Description | Default |
|---|---|---|
--drs-radius |
Overlay panel border radius | 10px |
--drs-radius-sm |
Button border radius | 5px |
--drs-sidebar-width |
Predefined-ranges sidebar width | 148px |
--drs-overlay-z |
z-index of the overlay panel | 1000 |
--drs-shadow |
Overlay panel box shadow | dark shadow |
--drs-bg |
Overlay / modal background | #1e1f22 |
--drs-trigger-bg |
Trigger button background | --drs-bg |
--drs-primary |
Accent / highlight colour | #f97316 |
--drs-primary-fg |
Foreground on accent colour | #ffffff |
--drs-text |
Primary text colour | #f1f1f1 |
--drs-text-muted |
Dimmed / secondary text | 35% opacity |
--drs-border |
Border and divider colour | 8% white |
--drs-hover |
Hover background | 7% white |
--drs-range-bg |
In-range day background | orange 14% |
--drs-time-bg |
Time-picker box background | 5% white |
--drs-font-family |
Font family | inherit |
--drs-font-size |
Base font size | 0.875rem |
--drs-header-font-size |
Month / year header size | 0.9375rem |
--drs-header-font-weight |
Month / year header weight | 700 |
--drs-weekday-font-size |
Day-of-week label size | 0.6875rem |
--drs-day-font-size |
Day number size | 0.875rem |
--drs-sidebar-font-size |
Predefined-range label size | 0.875rem |
--drs-time-font-size |
Hour / minute number size | 1.375rem |
--drs-ampm-font-size |
AM/PM toggle size | 0.9375rem |
--drs-label-font-size |
"Start time:" / "End time:" label | 0.8125rem |
--drs-trigger-font-size |
Trigger button text size | 0.875rem |
--drs-apply-font-size |
Apply button text size | 0.875rem |
// 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';# 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 testMIT