Date Picker

A date picker component that combines a button trigger with a calendar popup for intuitive date selection.

PreviousNext
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
 
import { ZardDatePickerComponent } from '../date-picker.component';
 
@Component({
  selector: 'zard-demo-date-picker-default',
  imports: [ZardDatePickerComponent],
  standalone: true,
  template: `
    <z-date-picker [value]="selectedDate()" placeholder="Pick a date" (dateChange)="onDateChange($event)" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDatePickerDefaultComponent {
  readonly selectedDate = signal<Date | null>(null);
 
  onDateChange(date: Date | null) {
    this.selectedDate.set(date);
    console.log('Selected date:', date);
  }
}
 

Installation

1

Run the CLI

Use the CLI to add the component to your project.

npx @ngzard/ui@latest add date-picker
1

Add the component files

Create the component directory structure and add the following files to your project.

date-picker.component.ts
date-picker.component.ts
import { DatePipe } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  forwardRef,
  inject,
  input,
  model,
  output,
  viewChild,
  ViewEncapsulation,
  type TemplateRef,
} from '@angular/core';
import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms';
 
import type { ClassValue } from 'clsx';
 
import { datePickerVariants, type ZardDatePickerVariants } from './date-picker.variants';
import { mergeClasses } from '../../shared/utils/utils';
import { ZardButtonComponent } from '../button/button.component';
import { ZardCalendarComponent } from '../calendar/calendar.component';
import { ZardIconComponent } from '../icon/icon.component';
import { ZardPopoverComponent, ZardPopoverDirective } from '../popover/popover.component';
 
const HEIGHT_BY_SIZE: Record<NonNullable<ZardDatePickerVariants['zSize']>, string> = {
  sm: 'h-8',
  default: 'h-10',
  lg: 'h-12',
};
 
@Component({
  selector: 'z-date-picker, [z-date-picker]',
  imports: [ZardButtonComponent, ZardCalendarComponent, ZardPopoverComponent, ZardPopoverDirective, ZardIconComponent],
  standalone: true,
  template: `
    <button
      z-button
      type="button"
      [zType]="zType()"
      [zSize]="zSize()"
      [disabled]="disabled()"
      [class]="buttonClasses()"
      zPopover
      #popoverDirective="zPopover"
      [zContent]="calendarTemplate"
      zTrigger="click"
      (zVisibleChange)="onPopoverVisibilityChange($event)"
      [attr.aria-expanded]="false"
      [attr.aria-haspopup]="true"
      aria-label="Choose date"
    >
      <z-icon zType="calendar" />
      <span [class]="textClasses()">
        {{ displayText() }}
      </span>
    </button>
 
    <ng-template #calendarTemplate>
      <z-popover [class]="popoverClasses()">
        <z-calendar
          #calendar
          class="border-0"
          [value]="value()"
          [minDate]="minDate()"
          [maxDate]="maxDate()"
          [disabled]="disabled()"
          (dateChange)="onDateChange($event)"
        />
      </z-popover>
    </ng-template>
  `,
  providers: [
    DatePipe,
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ZardDatePickerComponent),
      multi: true,
    },
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {},
  exportAs: 'zDatePicker',
})
export class ZardDatePickerComponent implements ControlValueAccessor {
  private readonly datePipe = inject(DatePipe);
 
  readonly calendarTemplate = viewChild.required<TemplateRef<unknown>>('calendarTemplate');
  readonly popoverDirective = viewChild.required<ZardPopoverDirective>('popoverDirective');
  readonly calendar = viewChild.required<ZardCalendarComponent>('calendar');
 
  readonly class = input<ClassValue>('');
  readonly zType = input<ZardDatePickerVariants['zType']>('outline');
  readonly zSize = input<ZardDatePickerVariants['zSize']>('default');
  readonly value = model<Date | null>(null);
  readonly placeholder = input<string>('Pick a date');
  readonly zFormat = input<string>('MMMM d, yyyy');
  readonly minDate = input<Date | null>(null);
  readonly maxDate = input<Date | null>(null);
  readonly disabled = model<boolean>(false);
 
  readonly dateChange = output<Date | null>();
 
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  private onChange: (value: Date | null) => void = () => {};
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  private onTouched: () => void = () => {};
 
  protected readonly classes = computed(() =>
    mergeClasses(
      datePickerVariants({
        zSize: this.zSize(),
      }),
      this.class(),
    ),
  );
 
  protected readonly buttonClasses = computed(() => {
    const hasValue = !!this.value();
    const size: NonNullable<ZardDatePickerVariants['zSize']> = this.zSize() ?? 'default';
    const height = HEIGHT_BY_SIZE[size];
    return mergeClasses(
      'justify-start text-left font-normal',
      !hasValue && 'text-muted-foreground',
      height,
      'min-w-[240px]',
    );
  });
 
  protected readonly textClasses = computed(() => {
    const hasValue = !!this.value();
    return mergeClasses(!hasValue && 'text-muted-foreground');
  });
 
  protected readonly popoverClasses = computed(() => mergeClasses('w-auto p-0'));
 
  protected readonly displayText = computed(() => {
    const date = this.value();
    if (!date) {
      return this.placeholder();
    }
    return this.formatDate(date, this.zFormat());
  });
 
  protected onDateChange(date: Date | Date[]): void {
    // Date picker always uses single mode, so we can safely cast
    const singleDate = Array.isArray(date) ? (date[0] ?? null) : date;
    this.value.set(singleDate);
    this.onChange(singleDate);
    this.onTouched();
    this.dateChange.emit(singleDate);
 
    this.popoverDirective().hide();
  }
 
  protected onPopoverVisibilityChange(visible: boolean): void {
    if (visible) {
      setTimeout(() => {
        if (this.calendar()) {
          this.calendar().resetNavigation();
        }
      });
    }
  }
 
  private formatDate(date: Date, format: string): string {
    return this.datePipe.transform(date, format) ?? '';
  }
 
  writeValue(value: Date | null): void {
    this.value.set(value);
  }
 
  registerOnChange(fn: (value: Date | null) => void): void {
    this.onChange = fn;
  }
 
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
 
  setDisabledState(isDisabled: boolean): void {
    this.disabled.set(isDisabled);
  }
}
 
date-picker.variants.ts
date-picker.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
 
const datePickerVariants = cva('', {
  variants: {
    zSize: {
      sm: '',
      default: '',
      lg: '',
    },
    zType: {
      default: '',
      outline: '',
      ghost: '',
    },
  },
  defaultVariants: {
    zSize: 'default',
    zType: 'outline',
  },
});
 
export { datePickerVariants };
export type ZardDatePickerVariants = VariantProps<typeof datePickerVariants>;
 

Examples

default

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
 
import { ZardDatePickerComponent } from '../date-picker.component';
 
@Component({
  selector: 'zard-demo-date-picker-default',
  imports: [ZardDatePickerComponent],
  standalone: true,
  template: `
    <z-date-picker [value]="selectedDate()" placeholder="Pick a date" (dateChange)="onDateChange($event)" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDatePickerDefaultComponent {
  readonly selectedDate = signal<Date | null>(null);
 
  onDateChange(date: Date | null) {
    this.selectedDate.set(date);
    console.log('Selected date:', date);
  }
}
 

sizes

Small

Default

Large

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
 
import { ZardDatePickerComponent } from '../date-picker.component';
 
@Component({
  selector: 'zard-demo-date-picker-sizes',
  imports: [ZardDatePickerComponent],
  standalone: true,
  template: `
    <div class="flex flex-col gap-4">
      <div class="space-y-2">
        <h4 class="text-sm font-medium">Small</h4>
        <z-date-picker
          zSize="sm"
          [value]="selectedDateSm()"
          placeholder="Pick a date"
          (dateChange)="onDateChangeSm($event)"
        />
      </div>
 
      <div class="space-y-2">
        <h4 class="text-sm font-medium">Default</h4>
        <z-date-picker
          zSize="default"
          [value]="selectedDateDefault()"
          placeholder="Pick a date"
          (dateChange)="onDateChangeDefault($event)"
        />
      </div>
 
      <div class="space-y-2">
        <h4 class="text-sm font-medium">Large</h4>
        <z-date-picker
          zSize="lg"
          [value]="selectedDateLg()"
          placeholder="Pick a date"
          (dateChange)="onDateChangeLg($event)"
        />
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDatePickerSizesComponent {
  readonly selectedDateSm = signal<Date | null>(null);
  readonly selectedDateDefault = signal<Date | null>(null);
  readonly selectedDateLg = signal<Date | null>(null);
 
  onDateChangeSm(date: Date | null) {
    this.selectedDateSm.set(date);
    console.log('Selected date (sm):', date);
  }
 
  onDateChangeDefault(date: Date | null) {
    this.selectedDateDefault.set(date);
    console.log('Selected date (default):', date);
  }
 
  onDateChangeLg(date: Date | null) {
    this.selectedDateLg.set(date);
    console.log('Selected date (lg):', date);
  }
}
 

formats

import { ChangeDetectionStrategy, Component } from '@angular/core';
 
import { ZardDatePickerComponent } from '../date-picker.component';
 
@Component({
  selector: 'z-date-picker-formats-demo',
  imports: [ZardDatePickerComponent],
  standalone: true,
  template: `
    <div class="flex flex-col gap-4">
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">Default Format (MMMM d, yyyy)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" />
      </div>
 
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">US Format (MM/dd/yyyy)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" zFormat="MM/dd/yyyy" />
      </div>
 
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">European Format (dd-MM-yyyy)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" zFormat="dd-MM-yyyy" />
      </div>
 
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">Short Format (MMM d, yy)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" zFormat="MMM d, yy" />
      </div>
 
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">With Day Name (EEE, MMMM d)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" zFormat="EEE, MMMM d" />
      </div>
 
      <div class="flex flex-col gap-2">
        <label class="text-sm font-medium">ISO Format (yyyy-MM-dd)</label>
        <z-date-picker [value]="selectedDate" (dateChange)="selectedDate = $event" zFormat="yyyy-MM-dd" />
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDatePickerFormatsComponent {
  selectedDate: Date | null = new Date();
}
 
export default ZardDatePickerFormatsComponent;
 

Date Picker API

ZardDatePickerComponent

A date picker component that provides an intuitive interface for date selection through a button trigger and calendar popup.

Properties

Property Type Default Description
class ClassValue '' Additional CSS classes
zType 'default' | 'outline' | 'ghost' 'outline' Button variant style
zSize 'sm' | 'default' | 'lg' 'default' Size of the date picker
value Date | null null Selected date value
placeholder string 'Pick a date' Placeholder text when no date is selected
zFormat string 'MMMM d, yyyy' Date format pattern
minDate Date | null null Minimum selectable date
maxDate Date | null null Maximum selectable date
disabled boolean false Whether the date picker is disabled

Format Patterns

The zFormat property supports the following patterns:

Pattern Description Example
yyyy 4-digit year 2024
yy 2-digit year 24
MMMM Full month name January
MMM Short month name Jan
MM Month (2 digits) 01
M Month (1-2 digits) 1
dd Day (2 digits) 05
d Day (1-2 digits) 5
EEEE Full day name Sunday
EEE Short day name Sun

Common format examples:

  • 'MMMM d, yyyy' → "January 5, 2024" (default)
  • 'MM/dd/yyyy' → "01/05/2024"
  • 'dd-MM-yy' → "05-01-24"
  • 'EEE, MMM d' → "Sun, Jan 5"

Events

Event Type Description
dateChange EventEmitter<Date | null> Emitted when a date is selected