calendar

A calendar component that allows users to select a date or a range of dates.

PreviousNext
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

@Component({
  selector: 'z-demo-calendar-preview',
  imports: [ZardCalendarComponent],
  template: `
    <z-calendar zMode="single" zCaptionLayout="dropdown" class="rounded-lg border" [(value)]="selectedDate" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarPreviewComponent {
  readonly selectedDate = signal<Date | null>(new Date());
}

Installation

Copy
npx zard-cli@latest add calendar

Usage

import { ZardCalendarComponent } from '@/shared/components/calendar/calendar.component';
Copy
<z-calendar zMode="single" class="rounded-lg border"></z-calendar>
Copy

Examples

basic

The calendar renders without a border by default — add class="rounded-lg border" to frame it.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

@Component({
  selector: 'z-demo-calendar-basic',
  imports: [ZardCalendarComponent],
  template: `
    <z-calendar zMode="single" class="rounded-lg border" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarBasicComponent {}

range

Use zMode="range" to let users select a start and an end date, and zNumberOfMonths to show more than one month at a time.
January 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
February 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

const RANGE_LENGTH_IN_DAYS = 30;

function startOfRange(): Date {
  return new Date(new Date().getFullYear(), 0, 12);
}

function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

@Component({
  selector: 'z-demo-calendar-range',
  imports: [ZardCalendarComponent],
  template: `
    <z-calendar zMode="range" zNumberOfMonths="2" class="rounded-lg border" [(value)]="dateRange" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarRangeComponent {
  readonly dateRange = signal<Date[] | null>([startOfRange(), addDays(startOfRange(), RANGE_LENGTH_IN_DAYS)]);
}

multiple

Use zMode="multiple" to select any number of individual dates.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat

Selected (0) date(s).

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

@Component({
  selector: 'z-demo-calendar-multiple',
  imports: [ZardCalendarComponent],
  template: `
    <div class="flex flex-col gap-4">
      <z-calendar zMode="multiple" class="rounded-lg border" [(value)]="selectedDates" />

      <p class="text-muted-foreground text-sm font-medium">Selected ({{ selectedDates()?.length ?? 0 }}) date(s).</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarMultipleComponent {
  readonly selectedDates = signal<Date[] | null>(null);
}

presets

Compose the calendar with a <z-card /> footer to offer quick date presets.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardCardImports } from '@/shared/components/card/card.imports';

import { ZardCalendarComponent } from '../calendar.component';

interface CalendarPreset {
  label: string;
  days: number;
}

const PRESETS: CalendarPreset[] = [
  { label: 'Today', days: 0 },
  { label: 'Tomorrow', days: 1 },
  { label: 'In 3 days', days: 3 },
  { label: 'In a week', days: 7 },
  { label: 'In 2 weeks', days: 14 },
];

@Component({
  selector: 'z-demo-calendar-presets',
  imports: [ZardCalendarComponent, ZardButtonComponent, ZardCardImports],
  template: `
    <z-card zSize="sm" class="mx-auto w-fit max-w-[300px]">
      <z-card-content>
        <z-calendar zMode="single" class="p-0 [--cell-size:--spacing(9.5)]" [(value)]="selectedDate" />
      </z-card-content>
      <z-card-footer zFooterBorder class="flex flex-wrap gap-2">
        @for (preset of presets; track preset.days) {
          <button z-button type="button" zType="outline" zSize="sm" class="flex-1" (click)="selectPreset(preset)">
            {{ preset.label }}
          </button>
        }
      </z-card-footer>
    </z-card>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarPresetsComponent {
  readonly presets = PRESETS;
  readonly selectedDate = signal<Date | null>(new Date());

  selectPreset(preset: CalendarPreset): void {
    const date = new Date();
    date.setDate(date.getDate() + preset.days);
    this.selectedDate.set(date);
  }
}

with time

Pair the calendar with time inputs to build a date and time picker.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideClock2 } from '@ng-icons/lucide';

import { ZardCardImports } from '@/shared/components/card/card.imports';
import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardInputGroupImports } from '@/shared/components/input-group/input-group.imports';

import { ZardCalendarComponent } from '../calendar.component';

@Component({
  selector: 'z-demo-calendar-with-time',
  imports: [
    ZardCalendarComponent,
    ZardCardImports,
    ZardFieldImports,
    ZardInputGroupImports,
    ZardInputComponent,
    NgIcon,
  ],
  template: `
    <z-card zSize="sm" class="mx-auto w-fit">
      <z-card-content>
        <z-calendar zMode="single" class="p-0" [(value)]="selectedDate" />
      </z-card-content>
      <z-card-footer zFooterBorder>
        <div z-field-group>
          <div z-field>
            <label z-field-label for="calendar-time-from">Start Time</label>
            <z-input-group>
              <input
                z-input
                id="calendar-time-from"
                type="time"
                step="1"
                value="10:30:00"
                class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden"
              />
              <z-input-group-addon>
                <ng-icon name="lucideClock2" class="text-muted-foreground" />
              </z-input-group-addon>
            </z-input-group>
          </div>

          <div z-field>
            <label z-field-label for="calendar-time-to">End Time</label>
            <z-input-group>
              <input
                z-input
                id="calendar-time-to"
                type="time"
                step="1"
                value="12:30:00"
                class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden"
              />
              <z-input-group-addon>
                <ng-icon name="lucideClock2" class="text-muted-foreground" />
              </z-input-group-addon>
            </z-input-group>
          </div>
        </div>
      </z-card-footer>
    </z-card>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  viewProviders: [provideIcons({ lucideClock2 })],
})
export class ZardDemoCalendarWithTimeComponent {
  readonly selectedDate = signal<Date | null>(new Date());
}

booked dates

Use zDisabledDates to block individual days — here they are struck through to read as booked.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

const FIRST_BOOKED_DAY = 8;
const BOOKED_DAYS_COUNT = 12;

function bookedDatesOfCurrentMonth(): Date[] {
  const today = new Date();

  return Array.from(
    { length: BOOKED_DAYS_COUNT },
    (_, index) => new Date(today.getFullYear(), today.getMonth(), FIRST_BOOKED_DAY + index),
  );
}

@Component({
  selector: 'z-demo-calendar-booked-dates',
  imports: [ZardCalendarComponent],
  template: `
    <z-calendar
      zMode="single"
      class="rounded-lg border shadow-sm [&_[data-disabled=true]_button]:line-through [&_[data-disabled=true]_button]:opacity-100"
      [zDisabledDates]="bookedDates"
      [(value)]="selectedDate"
    />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarBookedDatesComponent {
  readonly bookedDates = bookedDatesOfCurrentMonth();
  readonly selectedDate = signal<Date | null>(null);
}

custom cell size

Override the --cell-size CSS variable to resize the whole calendar, and pass a zDayTemplate to render extra content in each day — here a price that changes on weekends.
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCardImports } from '@/shared/components/card/card.imports';

import { ZardCalendarComponent } from '../calendar.component';

const RANGE_LENGTH_IN_DAYS = 10;

/** December 8 of the current year; the calendar opens on the month of the range start. */
function startOfRange(): Date {
  return new Date(new Date().getFullYear(), 11, 8);
}

function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

function isWeekend(date: Date): boolean {
  return date.getDay() === 0 || date.getDay() === 6;
}

@Component({
  selector: 'z-demo-calendar-custom-cell-size',
  imports: [ZardCalendarComponent, ZardCardImports],
  template: `
    <z-card class="mx-auto w-fit p-0">
      <z-card-content class="p-0">
        <z-calendar
          zMode="range"
          zCaptionLayout="dropdown"
          class="[--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)]"
          [zDayTemplate]="day"
          [(value)]="dateRange"
        />
      </z-card-content>
    </z-card>

    <ng-template #day let-day>
      {{ day.date.getDate() }}
      @if (day.isCurrentMonth) {
        <span>{{ isWeekend(day.date) ? '$120' : '$100' }}</span>
      }
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarCustomCellSizeComponent {
  readonly dateRange = signal<Date[] | null>([startOfRange(), addDays(startOfRange(), RANGE_LENGTH_IN_DAYS)]);

  protected readonly isWeekend = isWeekend;
}

with constraints

Use minDate and maxDate to limit the selectable range, or disabled to turn the whole calendar off.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

const DAYS_IN_FUTURE = 30;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;

@Component({
  selector: 'z-demo-calendar-with-constraints',
  imports: [ZardCalendarComponent],
  template: `
    <div class="flex flex-wrap items-start justify-center gap-6">
      <z-calendar zMode="single" class="rounded-lg border" [minDate]="minDate" [maxDate]="maxDate" />

      <z-calendar zMode="single" class="rounded-lg border" [disabled]="true" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarWithConstraintsComponent {
  readonly minDate = new Date();
  readonly maxDate = new Date(Date.now() + DAYS_IN_FUTURE * MILLISECONDS_PER_DAY);

  constructor() {
    this.minDate.setHours(0, 0, 0, 0);
    this.maxDate.setHours(23, 59, 59, 999);
  }
}

expand year selection range

minDate and maxDate also expand the year dropdown — useful for a date of birth picker.
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardCalendarComponent } from '../calendar.component';

const BIRTH_YEAR_FLOOR = 1950;

@Component({
  selector: 'z-demo-calendar-expand-year-selection-range',
  imports: [ZardCalendarComponent],
  template: `
    <z-calendar
      zMode="single"
      zCaptionLayout="dropdown"
      class="rounded-lg border"
      [minDate]="minDate"
      [maxDate]="maxDate"
      [(value)]="dateOfBirth"
    />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarExpandYearSelectionRangeComponent {
  readonly minDate = new Date(BIRTH_YEAR_FLOOR, 0, 1);
  readonly maxDate = new Date();
  readonly dateOfBirth = signal<Date | null>(null);
}

i18n

Localize the calendar by providing provideZardI18n() in your application config, or injecting ZardI18nService to switch locales dynamically at runtime. The calendar updates month names, weekday names, and the starting day of the week.
October 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardI18nService } from '@/shared/core/i18n';

import { ZardCalendarI18nService } from '../calendar-i18n.service';
import { ZardCalendarComponent } from '../calendar.component';

@Component({
  selector: 'z-demo-calendar-i18n',
  imports: [ZardCalendarComponent, ZardButtonComponent],
  template: `
    <div class="flex flex-col items-center gap-4">
      <div class="flex flex-wrap justify-center gap-2">
        @for (loc of locales; track loc.code) {
          <button
            z-button
            type="button"
            [zType]="currentLocale() === loc.code ? 'default' : 'outline'"
            zSize="sm"
            (click)="setLocale(loc.code)"
          >
            {{ loc.label }}
          </button>
        }
      </div>

      <z-calendar zMode="single" class="rounded-lg border" />
    </div>
  `,
  providers: [ZardI18nService, ZardCalendarI18nService],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoCalendarI18nComponent {
  private readonly i18n = inject(ZardI18nService);

  readonly locales = [
    { code: 'en-US', label: 'English (US)' },
    { code: 'fr-FR', label: 'Français' },
    { code: 'de-DE', label: 'Deutsch' },
    { code: 'es-ES', label: 'Español' },
    { code: 'it-IT', label: 'Italiano' },
    { code: 'pt-BR', label: 'Português' },
  ];

  readonly currentLocale = this.i18n.locale;

  setLocale(code: string): void {
    this.i18n.setLocale(code);
  }
}

Setting the locale code

By default, provideZardI18n() uses the existing LOCALE_ID locale code from @angular/core or en-US. If you want to override it, provide a new value for the locale code:
app.config.ts Copy
import { ApplicationConfig } from '@angular/core';
import { provideZardI18n } from '@/shared/core/i18n';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZardI18n('pt-BR'),
  ],
};

API Reference

z-calendarComponent

A calendar component that allows users to select a date or a range of dates, with full keyboard navigation support. Every measurement derives from the --cell-size and --cell-radius CSS variables listed at the end of the table, so overriding them through the class input rescales the whole calendar.

PropertyDescriptionTypeDefault
[class] Additional CSS classes. Also where a border is opted into (`rounded-lg border`) and where the CSS variables below are overridden ClassValue ''
[zMode] Selection mode of the calendar 'single' | 'multiple' | 'range' 'single'
[value] Currently selected date(s) - type depends on mode CalendarValue null
[minDate] Minimum selectable date. Also used to expand the year picker range Date | null null
[maxDate] Maximum selectable date. Also used to expand the year picker range Date | null null
[disabled] Whether the calendar is disabled boolean false
[zCaptionLayout] How the month/year caption is rendered: a plain label, two dropdowns, or a dropdown for only the month or only the year. The dropdowns are native `<select>` elements laid invisible over the label, so the browser owns the popup 'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years' 'label'
[zButtonVariant] Button variant used by the previous/next month arrows ZardButtonTypeVariants 'ghost'
[zShowOutsideDays] Whether the days of the surrounding months are visible. When false they are hidden but keep their grid cell, so the layout never shifts boolean true
[zDisabledDates] Individual days that cannot be selected, on top of the minDate/maxDate range. Each day still keeps its grid cell and is marked with `data-disabled="true"` Date[] []
[zNumberOfMonths] How many months are rendered side by side. They stack vertically below the `md` breakpoint, and only the first and the last month carry the navigation arrows number 1
[zDayTemplate] Template rendered inside every day button instead of the plain day number. The `CalendarDay` is the implicit context (`let-day`), so the template can read `date`, `isCurrentMonth`, `isSelected` and the range flags. A second `<span>` inside the button is styled as a small caption TemplateRef<CalendarDayTemplateContext> | null null
(dateChange) Emitted when date selection changes EventEmitter<Date | Date[]> -
resetNavigation() Public method that moves the visible month back to the selected value and clears the roving focus () => void -
[--cell-size] CSS variable: width and height of a day cell, e.g. `class="[--cell-size:--spacing(12)]"` length --spacing(7)
[--cell-radius] CSS variable: corner radius of a day cell, e.g. `class="[--cell-radius:var(--radius-lg)]"` length var(--radius-md)

ZardCalendarI18nServiceComponent

Injectable service that provides localized month names, weekday names, and week-start configuration. Driven by ZardI18nService or provideZardI18n().

PropertyDescriptionTypeDefault
locale Signal emitting the current BCP-47 locale tag, e.g. "en-US", "fr-FR" Signal<string> 'en-US'
weekStartsOn Signal emitting the starting day of the week (0 for Sunday, 1 for Monday, etc.) Signal<ZardDayOfWeek> 0
shortMonths Signal emitting the 12 localized short month names for the current locale Signal<string[]>
longMonths Signal emitting the 12 localized full month names for the current locale Signal<string[]>
weekdays Signal emitting the 7 localized weekday names matching weekStartsOn Signal<string[]>
getMonthNames(format?, locale?) Returns formatted month names for a given locale (format?: 'short' | 'long', locale?: string) => string[]
getWeekdayNames(format?, weekStartsOn?, locale?) Returns formatted weekday names for a given locale and starting day (format?: 'short' | 'narrow' | 'long', weekStartsOn?: ZardDayOfWeek, locale?: string) => string[]
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .