Skip to content
Ndop UI

Calendar

Month grids to choose a date, a range or several dates.

October 2026
Sun Mon Tue Wed Thu Fri Sat
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
} from '@/ui/calendar/calendar';

/**
 * A calendar to choose one date. Today is marked; the arrow keys move between
 * days and Page Up and Page Down between months.
 */
@Component({
  selector: 'example-calendar-demo',
  imports: [
    Calendar,
    CalendarCaption,
    CalendarCell,
    CalendarDay,
    CalendarGrid,
    CalendarHeading,
    CalendarMonth,
    CalendarMonths,
    CalendarNav,
    CalendarNext,
    CalendarPrevious,
    CalendarWeek,
    CalendarWeekday,
    CalendarWeekdays,
    NdIcon,
  ],
  template: `
    <div
      ndCalendar
      #calendar="ndCalendar"
      [(value)]="date"
      class="rounded-md border shadow-sm"
    >
      <div ndCalendarMonths>
        <div ndCalendarNav>
          <button ndCalendarPrevious aria-label="Previous month">
            <svg ndIcon="chevron-left"></svg>
          </button>
          <button ndCalendarNext aria-label="Next month">
            <svg ndIcon="chevron-right"></svg>
          </button>
        </div>
        @for (month of calendar.months(); track month.key) {
          <div ndCalendarMonth [month]="month">
            <div ndCalendarCaption>
              <div ndCalendarHeading>{{ month.label }}</div>
            </div>
            <table ndCalendarGrid>
              <thead>
                <tr ndCalendarWeekdays>
                  @for (weekday of calendar.weekdays(); track weekday.long) {
                    <th ndCalendarWeekday [weekday]="weekday">
                      {{ weekday.short }}
                    </th>
                  }
                </tr>
              </thead>
              <tbody>
                @for (week of month.weeks; track week.key) {
                  <tr ndCalendarWeek>
                    @for (day of week.days; track day.key) {
                      <td ndCalendarCell [day]="day">
                        <button ndCalendarDay [day]="day">
                          {{ day.label }}
                        </button>
                      </td>
                    }
                  </tr>
                }
              </tbody>
            </table>
          </div>
        }
      </div>
    </div>
  `,
})
export class CalendarDemoExample {
  protected readonly date = signal<Date | null>(new Date());
}

A calendar shows month grids to choose a date, a range of days or several dates. Only one day is in the tab order; the arrow keys move between days, Page Up and Page Down between months, and Enter or Space chooses a day. Month names, weekdays and day labels come from Intl in the app's locale, and weeks start on the locale's first day. It binds forms like any control.

Installation

npx @ndop/cli add calendar

Usage

import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
} from '@/ui/calendar/calendar';
<div
  ndCalendar
  #calendar="ndCalendar"
  [(value)]="date"
  class="rounded-md border shadow-sm"
>
  <div ndCalendarMonths>
    <div ndCalendarNav>
      <button ndCalendarPrevious aria-label="Previous month">
        <svg ndIcon="chevron-left"></svg>
      </button>
      <button ndCalendarNext aria-label="Next month">
        <svg ndIcon="chevron-right"></svg>
      </button>
    </div>
    @for (month of calendar.months(); track month.key) {
      <div ndCalendarMonth [month]="month">
        <div ndCalendarCaption>
          <div ndCalendarHeading>{{ month.label }}</div>
        </div>
        <table ndCalendarGrid>
          <thead>
            <tr ndCalendarWeekdays>
              @for (weekday of calendar.weekdays(); track weekday.long) {
                <th ndCalendarWeekday [weekday]="weekday">
                  {{ weekday.short }}
                </th>
              }
            </tr>
          </thead>
          <tbody>
            @for (week of month.weeks; track week.key) {
              <tr ndCalendarWeek>
                @for (day of week.days; track day.key) {
                  <td ndCalendarCell [day]="day">
                    <button ndCalendarDay [day]="day">
                      {{ day.label }}
                    </button>
                  </td>
                }
              </tr>
            }
          </tbody>
        </table>
      </div>
    }
  </div>
</div>

Examples

Calendar range

Two months side by side to choose a range of days.

October 2026
Sun Mon Tue Wed Thu Fri Sat
November 2026
Sun Mon Tue Wed Thu Fri Sat
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
  type DateRange,
} from '@/ui/calendar/calendar';

/**
 * Two months side by side to choose a range of days: the first click
 * starts it, the second ends it.
 */
@Component({
  selector: 'example-calendar-range',
  imports: [
    Calendar,
    CalendarCaption,
    CalendarCell,
    CalendarDay,
    CalendarGrid,
    CalendarHeading,
    CalendarMonth,
    CalendarMonths,
    CalendarNav,
    CalendarNext,
    CalendarPrevious,
    CalendarWeek,
    CalendarWeekday,
    CalendarWeekdays,
    NdIcon,
  ],
  template: `
    <div
      ndCalendar
      #calendar="ndCalendar"
      mode="range"
      [numberOfMonths]="2"
      [(range)]="range"
      class="rounded-md border shadow-sm"
    >
      <div ndCalendarMonths>
        <div ndCalendarNav>
          <button ndCalendarPrevious aria-label="Previous month">
            <svg ndIcon="chevron-left"></svg>
          </button>
          <button ndCalendarNext aria-label="Next month">
            <svg ndIcon="chevron-right"></svg>
          </button>
        </div>
        @for (month of calendar.months(); track month.key) {
          <div ndCalendarMonth [month]="month">
            <div ndCalendarCaption>
              <div ndCalendarHeading>{{ month.label }}</div>
            </div>
            <table ndCalendarGrid>
              <thead>
                <tr ndCalendarWeekdays>
                  @for (weekday of calendar.weekdays(); track weekday.long) {
                    <th ndCalendarWeekday [weekday]="weekday">
                      {{ weekday.short }}
                    </th>
                  }
                </tr>
              </thead>
              <tbody>
                @for (week of month.weeks; track week.key) {
                  <tr ndCalendarWeek>
                    @for (day of week.days; track day.key) {
                      <td ndCalendarCell [day]="day">
                        <button ndCalendarDay [day]="day">
                          {{ day.label }}
                        </button>
                      </td>
                    }
                  </tr>
                }
              </tbody>
            </table>
          </div>
        }
      </div>
    </div>
  `,
})
export class CalendarRangeExample {
  protected readonly range = signal<DateRange>({
    from: new Date(),
    to: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000),
  });
}

Calendar multiple

Several dates chosen at once.

October 2026
Sun Mon Tue Wed Thu Fri Sat

0 dates chosen.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
} from '@/ui/calendar/calendar';

/**
 * Several dates chosen at once; choosing a date again removes it.
 */
@Component({
  selector: 'example-calendar-multiple',
  imports: [
    Calendar,
    CalendarCaption,
    CalendarCell,
    CalendarDay,
    CalendarGrid,
    CalendarHeading,
    CalendarMonth,
    CalendarMonths,
    CalendarNav,
    CalendarNext,
    CalendarPrevious,
    CalendarWeek,
    CalendarWeekday,
    CalendarWeekdays,
    NdIcon,
  ],
  template: `
    <div class="grid justify-items-center gap-3">
      <div
        ndCalendar
        #calendar="ndCalendar"
        mode="multiple"
        [(values)]="dates"
        class="rounded-md border shadow-sm"
      >
        <div ndCalendarMonths>
          <div ndCalendarNav>
            <button ndCalendarPrevious aria-label="Previous month">
              <svg ndIcon="chevron-left"></svg>
            </button>
            <button ndCalendarNext aria-label="Next month">
              <svg ndIcon="chevron-right"></svg>
            </button>
          </div>
          @for (month of calendar.months(); track month.key) {
            <div ndCalendarMonth [month]="month">
              <div ndCalendarCaption>
                <div ndCalendarHeading>{{ month.label }}</div>
              </div>
              <table ndCalendarGrid>
                <thead>
                  <tr ndCalendarWeekdays>
                    @for (weekday of calendar.weekdays(); track weekday.long) {
                      <th ndCalendarWeekday [weekday]="weekday">
                        {{ weekday.short }}
                      </th>
                    }
                  </tr>
                </thead>
                <tbody>
                  @for (week of month.weeks; track week.key) {
                    <tr ndCalendarWeek>
                      @for (day of week.days; track day.key) {
                        <td ndCalendarCell [day]="day">
                          <button ndCalendarDay [day]="day">
                            {{ day.label }}
                          </button>
                        </td>
                      }
                    </tr>
                  }
                </tbody>
              </table>
            </div>
          }
        </div>
      </div>
      <p class="text-sm text-muted-foreground">
        {{ dates().length }} dates chosen.
      </p>
    </div>
  `,
})
export class CalendarMultipleExample {
  protected readonly dates = signal<readonly Date[]>([]);
}

Calendar disabled

Weekends and past days that can't be chosen.

October 2026
Sun Mon Tue Wed Thu Fri Sat
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
} from '@/ui/calendar/calendar';

/**
 * Weekends and past days that can't be chosen. They stay focusable, so
 * the arrow keys move across them.
 */
@Component({
  selector: 'example-calendar-disabled',
  imports: [
    Calendar,
    CalendarCaption,
    CalendarCell,
    CalendarDay,
    CalendarGrid,
    CalendarHeading,
    CalendarMonth,
    CalendarMonths,
    CalendarNav,
    CalendarNext,
    CalendarPrevious,
    CalendarWeek,
    CalendarWeekday,
    CalendarWeekdays,
    NdIcon,
  ],
  template: `
    <div
      ndCalendar
      #calendar="ndCalendar"
      [min]="today"
      [disabledDates]="weekends"
      class="rounded-md border shadow-sm"
    >
      <div ndCalendarMonths>
        <div ndCalendarNav>
          <button ndCalendarPrevious aria-label="Previous month">
            <svg ndIcon="chevron-left"></svg>
          </button>
          <button ndCalendarNext aria-label="Next month">
            <svg ndIcon="chevron-right"></svg>
          </button>
        </div>
        @for (month of calendar.months(); track month.key) {
          <div ndCalendarMonth [month]="month">
            <div ndCalendarCaption>
              <div ndCalendarHeading>{{ month.label }}</div>
            </div>
            <table ndCalendarGrid>
              <thead>
                <tr ndCalendarWeekdays>
                  @for (weekday of calendar.weekdays(); track weekday.long) {
                    <th ndCalendarWeekday [weekday]="weekday">
                      {{ weekday.short }}
                    </th>
                  }
                </tr>
              </thead>
              <tbody>
                @for (week of month.weeks; track week.key) {
                  <tr ndCalendarWeek>
                    @for (day of week.days; track day.key) {
                      <td ndCalendarCell [day]="day">
                        <button ndCalendarDay [day]="day">
                          {{ day.label }}
                        </button>
                      </td>
                    }
                  </tr>
                }
              </tbody>
            </table>
          </div>
        }
      </div>
    </div>
  `,
})
export class CalendarDisabledExample {
  protected readonly today = new Date();
  protected readonly weekends = (date: Date) =>
    date.getDay() === 0 || date.getDay() === 6;
}

Calendar locale

Month and weekday names in French, with weeks from Monday.

octobre 2026
lun. mar. mer. jeu. ven. sam. dim.
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Calendar,
  CalendarCaption,
  CalendarCell,
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarMonth,
  CalendarMonths,
  CalendarNav,
  CalendarNext,
  CalendarPrevious,
  CalendarWeek,
  CalendarWeekday,
  CalendarWeekdays,
} from '@/ui/calendar/calendar';

/**
 * Month and weekday names in French, with weeks from Monday, as French
 * calendars start them. Names and labels come from Intl.
 */
@Component({
  selector: 'example-calendar-locale',
  imports: [
    Calendar,
    CalendarCaption,
    CalendarCell,
    CalendarDay,
    CalendarGrid,
    CalendarHeading,
    CalendarMonth,
    CalendarMonths,
    CalendarNav,
    CalendarNext,
    CalendarPrevious,
    CalendarWeek,
    CalendarWeekday,
    CalendarWeekdays,
    NdIcon,
  ],
  template: `
    <div
      ndCalendar
      #calendar="ndCalendar"
      locale="fr-FR"
      [(value)]="date"
      class="rounded-md border shadow-sm"
    >
      <div ndCalendarMonths>
        <div ndCalendarNav>
          <button ndCalendarPrevious aria-label="Previous month">
            <svg ndIcon="chevron-left"></svg>
          </button>
          <button ndCalendarNext aria-label="Next month">
            <svg ndIcon="chevron-right"></svg>
          </button>
        </div>
        @for (month of calendar.months(); track month.key) {
          <div ndCalendarMonth [month]="month">
            <div ndCalendarCaption>
              <div ndCalendarHeading>{{ month.label }}</div>
            </div>
            <table ndCalendarGrid>
              <thead>
                <tr ndCalendarWeekdays>
                  @for (weekday of calendar.weekdays(); track weekday.long) {
                    <th ndCalendarWeekday [weekday]="weekday">
                      {{ weekday.short }}
                    </th>
                  }
                </tr>
              </thead>
              <tbody>
                @for (week of month.weeks; track week.key) {
                  <tr ndCalendarWeek>
                    @for (day of week.days; track day.key) {
                      <td ndCalendarCell [day]="day">
                        <button ndCalendarDay [day]="day">
                          {{ day.label }}
                        </button>
                      </td>
                    }
                  </tr>
                }
              </tbody>
            </table>
          </div>
        }
      </div>
    </div>
  `,
})
export class CalendarLocaleExample {
  protected readonly date = signal<Date | null>(null);
}

Accessibility

Implements the Date grid pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
TabMoves focus to the day in the tab order, then out of the grid.
ArrowRightorArrowLeftMoves focus to the next or previous day, changing month when needed.
ArrowDownorArrowUpMoves focus to the same day of the next or previous week.
HomeorEndMoves focus to the first or last day of the week.
PageDownorPageUpMoves focus to the same day of the next or previous month.
Shift+PageDownorShift+PageUpMoves focus to the same day of the next or previous year.
EnterorSpaceChooses the focused day, unless it's disabled.

Focus

  • Focus never leaves the shown months on its own: moving past their edge shows the next or previous month and focuses the day there.
  • min and max stop the keys at their bounds.
  • Choosing a day keeps focus on it.

Right-to-left

In right-to-left layouts, the days run from right to left, so ArrowLeft moves to the next day and ArrowRight to the previous one. Direction comes from the nearest dir attribute.

API reference

Calendar

[ndCalendar]

A calendar (@ndop/primitives/calendar): month grids to choose a date, a range or several dates, with the arrow keys moving between days. Its structure is your template: loop over months() and weekdays().

Calendar binding Type Default
[class]
Extra classes, merged last so they win.
string''
[mode]
How it chooses dates.
'single' | 'multiple' | 'range''single'
[defaultMonth]
The first month shown when month isn't bound.
Date | nullnull
[numberOfMonths]
How many months show side by side.
number1
[min]
The earliest day that can be chosen.
Date | nullnull
[max]
The latest day that can be chosen.
Date | nullnull
[disabledDates]
Disables days it returns true for, such as weekends.
((date: Date) => boolean) | nullnull
[weekStartsOn]
The first weekday, 0 for Sunday; defaults to the locale's.
number | nullnull
[locale]
The locale for names and labels; defaults to the app's LOCALE_ID.
stringinject(LOCALE_ID)
[showOutsideDays]
Shows the neighboring months' days that fill the first and last weeks.
booleantrue
[(value)]
The chosen date in single mode. Two-way bindable with [(value)].
Date | nullnull
[(range)]
The chosen days in range mode. Two-way bindable with [(range)].
NdDateRange{ from: null, to: null }
[(values)]
The chosen dates in multiple mode. Two-way bindable with [(values)].
readonly Date[][]
[(month)]
The first month shown. Two-way bindable with [(month)].
Date | nullnull
Data attribute Values
data-slotcalendar
data-mode —

CalendarMonths

[ndCalendarMonths]

The row of shown months, which the navigation buttons sit above.

CalendarMonths binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-months

CalendarNav

[ndCalendarNav]

The bar that holds the previous and next buttons, across the top of the months.

CalendarNav binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-nav

CalendarPrevious

button[ndCalendarPrevious]

The button that shows the previous month. Name it with aria-label and put the chevron icon in it.

Manages aria-disabled.

CalendarPrevious binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-previous
data-disabled —

CalendarNext

button[ndCalendarNext]

The button that shows the next month. Name it with aria-label and put the chevron icon in it.

Manages aria-disabled.

CalendarNext binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-next
data-disabled —

CalendarMonth

[ndCalendarMonth]

One shown month: its caption and grid.

CalendarMonth binding Type Default
[class]
Extra classes, merged last so they win.
string''
[month]required
The month, from NdCalendar.months.
NdCalendarMonthView—
Data attribute Values
data-slotcalendar-month

CalendarCaption

[ndCalendarCaption]

The row of a month's name, between the navigation buttons.

CalendarCaption binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-caption

CalendarHeading

[ndCalendarHeading]

A month's name and year, which labels its grid and is announced when the month changes.

Manages id, aria-live.

CalendarHeading binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-heading

CalendarGrid

table[ndCalendarGrid]role grid

A month's grid of days, on a table.

Manages aria-labelledby, aria-multiselectable.

CalendarGrid binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-grid

CalendarWeekdays

tr[ndCalendarWeekdays]

The row of weekday headers, on a tr in the grid's thead.

CalendarWeekdays binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-weekdays

CalendarWeekday

th[ndCalendarWeekday]

A weekday's column header, with its full name for screen readers.

Manages abbr, scope.

CalendarWeekday binding Type Default
[class]
Extra classes, merged last so they win.
string''
[weekday]required
The weekday, from NdCalendar.weekdays.
NdCalendarWeekdayView—
Data attribute Values
data-slotcalendar-weekday

CalendarWeek

tr[ndCalendarWeek]

A week's row of days, on a tr in the grid's tbody.

CalendarWeek binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcalendar-week

CalendarCell

td[ndCalendarCell]role gridcell

A day's cell, which shapes the band of a chosen range.

Manages aria-selected.

CalendarCell binding Type Default
[class]
Extra classes, merged last so they win.
string''
[day]required
The day, from a week of NdCalendar.months.
NdCalendarDayView—
Data attribute Values
data-slotcalendar-cell
data-selected —
data-today —
data-outside —
data-disabled —
data-range-start —
data-range-middle —
data-range-end —

CalendarDay

button[ndCalendarDay]

A day's button: chosen days fill with the primary color, and a range's ends round off its band.

Manages aria-label, aria-current, aria-disabled, tabindex.

CalendarDay binding Type Default
[class]
Extra classes, merged last so they win.
string''
[day]required
The day, from a week of NdCalendar.months.
NdCalendarDayView—
Data attribute Values
data-slotcalendar-day
data-selected-single —
data-range-start —
data-range-middle —
data-range-end —
data-today —
data-outside —
data-disabled —

Agent notes

Do

  • Name the previous and next buttons with aria-label.
  • Use min, max and disabledDates for days that can't be chosen; they stay reachable with the keyboard.
  • Show two months with numberOfMonths="2" for ranges.

Don't

  • Don't use it for dates far from today, such as a birth date; use a text field.
  • Don't put it in a narrow space; put it in a Date Picker's popover.