# Calendar

Use `ui-calendar` when users need a visible calendar surface instead of a compact field. It works for booking, review scheduling, availability picking, and range selection where the month context matters and the user benefits from seeing adjacent days.

## Import
```ts
import { CalendarComponent, type CalendarDay, type CalendarView } from 'ui';
```

## Basic single-date selection
```ts
import { Component, computed, signal } from '@angular/core';
import { ButtonComponent, CalendarComponent, CalendarDay } from 'ui';

@Component({
  selector: 'app-calendar-basic-demo',
  standalone: true,
  imports: [ButtonComponent, CalendarComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;max-width:22rem">
      <ui-calendar
        [currentMonth]="currentMonth()"
        [selectedDate]="selectedDate()"
        [calendarView]="'days'"
        [size]="'medium'"
        [showMonthYearPicker]="true"
        (dateSelect)="onDateSelect($event)"
        (previousMonth)="shiftMonth(-1)"
        (nextMonth)="shiftMonth(1)"
      />

      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
      >
        <ui-button type="button" appearance="subtle" (click)="reset()">Clear date</ui-button>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
          {{ summary() }}
        </span>
      </div>
    </div>
  `,
})
export class CalendarBasicDemoComponent {
  protected readonly currentMonth = signal(new Date(2026, 4, 1));
  protected readonly selectedDate = signal<Date | null>(new Date(2026, 4, 14));

  protected readonly summary = computed(() =>
    this.selectedDate()
      ? `Selected date: ${this.selectedDate()!.toLocaleDateString('en-US', { dateStyle: 'medium' })}`
      : 'No date selected.',
  );

  protected onDateSelect(day: CalendarDay): void {
    this.selectedDate.set(day.date);
  }

  protected shiftMonth(delta: number): void {
    const next = new Date(this.currentMonth());
    next.setMonth(next.getMonth() + delta);
    this.currentMonth.set(next);
  }

  protected reset(): void {
    this.selectedDate.set(null);
    this.currentMonth.set(new Date(2026, 4, 1));
  }
}
```

## Days, months, and years views
```ts
import { Component, signal } from '@angular/core';
import { CalendarComponent, CalendarDay, CalendarView } from 'ui';

@Component({
  selector: 'app-calendar-views-demo',
  standalone: true,
  imports: [CalendarComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;align-items:start"
    >
      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Days view</div>
        <ui-calendar
          [currentMonth]="daysMonth()"
          [selectedDate]="selectedDate()"
          [calendarView]="'days'"
          [size]="'small'"
          (dateSelect)="onDateSelect($event)"
          (previousMonth)="shiftMonth(daysMonth, -1)"
          (nextMonth)="shiftMonth(daysMonth, 1)"
          (switchToMonthsView)="setView(daysView, 'months')"
        />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Months view</div>
        <ui-calendar
          [currentMonth]="monthsMonth()"
          [selectedDate]="selectedDate()"
          [calendarView]="'months'"
          [size]="'small'"
          (monthSelect)="onMonthSelect($event)"
          (previousYear)="shiftYear(monthsMonth, -1)"
          (nextYear)="shiftYear(monthsMonth, 1)"
          (switchToYearsView)="setView(monthsView, 'years')"
          (switchToDaysView)="setView(monthsView, 'days')"
        />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Years view</div>
        <ui-calendar
          [currentMonth]="yearsMonth()"
          [selectedDate]="selectedDate()"
          [calendarView]="'years'"
          [size]="'small'"
          (yearSelect)="onYearSelect($event)"
          (previousYearRange)="shiftYear(yearsMonth, -12)"
          (nextYearRange)="shiftYear(yearsMonth, 12)"
          (switchToMonthsView)="setView(yearsView, 'months')"
        />
      </div>
    </div>
  `,
})
export class CalendarViewsDemoComponent {
  protected readonly selectedDate = signal(new Date(2026, 4, 14));
  protected readonly daysMonth = signal(new Date(2026, 4, 1));
  protected readonly monthsMonth = signal(new Date(2026, 4, 1));
  protected readonly yearsMonth = signal(new Date(2026, 4, 1));
  protected readonly daysView = signal<CalendarView>('days');
  protected readonly monthsView = signal<CalendarView>('months');
  protected readonly yearsView = signal<CalendarView>('years');

  protected onDateSelect(day: CalendarDay): void {
    this.selectedDate.set(day.date);
  }

  protected onMonthSelect(monthIndex: number): void {
    const next = new Date(this.monthsMonth());
    next.setMonth(monthIndex);
    this.monthsMonth.set(next);
  }

  protected onYearSelect(year: number): void {
    const next = new Date(this.yearsMonth());
    next.setFullYear(year);
    this.yearsMonth.set(next);
  }

  protected shiftMonth(target: ReturnType<typeof signal<Date>>, delta: number): void {
    const next = new Date(target());
    next.setMonth(next.getMonth() + delta);
    target.set(next);
  }

  protected shiftYear(target: ReturnType<typeof signal<Date>>, delta: number): void {
    const next = new Date(target());
    next.setFullYear(next.getFullYear() + delta);
    target.set(next);
  }

  protected setView(target: ReturnType<typeof signal<CalendarView>>, view: CalendarView): void {
    target.set(view);
  }
}
```

## Sizes and month or year picker
```ts
import { Component, signal } from '@angular/core';
import { CalendarComponent, CalendarDay } from 'ui';

@Component({
  selector: 'app-calendar-size-picker-demo',
  standalone: true,
  imports: [CalendarComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;align-items:start"
    >
      @for (item of examples; track item.title) {
        <div
          style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <div style="display:flex;flex-direction:column;gap:0.25rem">
            <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
            <div style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
              {{ item.note }}
            </div>
          </div>

          <ui-calendar
            [currentMonth]="currentMonth()"
            [selectedDate]="selectedDate()"
            [calendarView]="'days'"
            [size]="item.size"
            [showMonthYearPicker]="item.showMonthYearPicker"
            (dateSelect)="onDateSelect($event)"
            (previousMonth)="shiftMonth(-1)"
            (nextMonth)="shiftMonth(1)"
          />
        </div>
      }
    </div>
  `,
})
export class CalendarSizePickerDemoComponent {
  protected readonly currentMonth = signal(new Date(2026, 4, 1));
  protected readonly selectedDate = signal<Date | null>(new Date(2026, 4, 18));

  protected readonly examples = [
    {
      title: 'Small with picker',
      note: 'Compact date selection inside denser forms or filters.',
      size: 'small' as const,
      showMonthYearPicker: true,
    },
    {
      title: 'Medium without picker',
      note: 'Keeps the header quieter when month switching should stay button driven.',
      size: 'medium' as const,
      showMonthYearPicker: false,
    },
    {
      title: 'Large with picker',
      note: 'Useful for touch-friendly or emphasized scheduling surfaces.',
      size: 'large' as const,
      showMonthYearPicker: true,
    },
  ];

  protected onDateSelect(day: CalendarDay): void {
    this.selectedDate.set(day.date);
  }

  protected shiftMonth(delta: number): void {
    const next = new Date(this.currentMonth());
    next.setMonth(next.getMonth() + delta);
    this.currentMonth.set(next);
  }
}
```

## Min and max constraints
```ts
import { Component, computed, signal } from '@angular/core';
import { ButtonComponent, CalendarComponent, CalendarDay } from 'ui';

@Component({
  selector: 'app-calendar-constraints-demo',
  standalone: true,
  imports: [ButtonComponent, CalendarComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;max-width:22rem">
      <ui-calendar
        [currentMonth]="currentMonth()"
        [selectedDate]="selectedDate()"
        [calendarView]="'days'"
        [size]="'medium'"
        [min]="minDate"
        [max]="maxDate"
        (dateSelect)="onDateSelect($event)"
        (previousMonth)="shiftMonth(-1)"
        (nextMonth)="shiftMonth(1)"
      />

      <div
        style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
      >
        <div style="display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center">
          <ui-button type="button" appearance="subtle" (click)="reset()">Reset window</ui-button>
          <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
            Allowed dates: {{ minDate }} to {{ maxDate }}
          </span>
        </div>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
          {{ summary() }}
        </span>
      </div>
    </div>
  `,
})
export class CalendarConstraintsDemoComponent {
  protected readonly minDate = '2026-05-12';
  protected readonly maxDate = '2026-05-26';
  protected readonly currentMonth = signal(new Date(2026, 4, 1));
  protected readonly selectedDate = signal<Date | null>(new Date(2026, 4, 14));

  protected readonly summary = computed(() =>
    this.selectedDate()
      ? `Chosen date: ${this.selectedDate()!.toLocaleDateString('en-US', { dateStyle: 'medium' })}`
      : 'Select any available date inside the allowed window.',
  );

  protected onDateSelect(day: CalendarDay): void {
    this.selectedDate.set(day.date);
  }

  protected shiftMonth(delta: number): void {
    const next = new Date(this.currentMonth());
    next.setMonth(next.getMonth() + delta);
    this.currentMonth.set(next);
  }

  protected reset(): void {
    this.currentMonth.set(new Date(2026, 4, 1));
    this.selectedDate.set(new Date(2026, 4, 14));
  }
}
```

## Range selection with hover preview
```ts
import { Component, computed, signal } from '@angular/core';
import { ButtonComponent, CalendarComponent, CalendarDay } from 'ui';

@Component({
  selector: 'app-calendar-range-demo',
  standalone: true,
  imports: [ButtonComponent, CalendarComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;max-width:22rem">
      <ui-calendar
        [currentMonth]="currentMonth()"
        [selectedDate]="null"
        [startDate]="startDate()"
        [endDate]="endDate()"
        [hoveredDate]="hoveredDate()"
        [calendarView]="'days'"
        [size]="'medium'"
        [showMonthYearPicker]="true"
        [isDayInHoverRangeFn]="isDayInHoverRange.bind(this)"
        (dateSelect)="onDateSelect($event)"
        (dateHover)="onDateHover($event)"
        (dateLeave)="onDateLeave()"
        (previousMonth)="shiftMonth(-1)"
        (nextMonth)="shiftMonth(1)"
      />

      <div
        style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
      >
        <div style="display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center">
          <ui-button type="button" appearance="subtle" (click)="reset()">Reset range</ui-button>
          <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
            Pick a start date, then an end date.
          </span>
        </div>
        <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
          {{ summary() }}
        </span>
      </div>
    </div>
  `,
})
export class CalendarRangeDemoComponent {
  protected readonly currentMonth = signal(new Date(2026, 6, 1));
  protected readonly startDate = signal<Date | null>(new Date(2026, 6, 10));
  protected readonly endDate = signal<Date | null>(new Date(2026, 6, 14));
  protected readonly hoveredDate = signal<Date | null>(null);
  protected readonly activeSelection = signal<'start' | 'end'>('start');

  protected readonly summary = computed(() => {
    const start = this.startDate();
    const end = this.endDate();
    if (!start && !end) {
      return 'No range selected.';
    }

    return `Range: ${start ? start.toLocaleDateString('en-US', { dateStyle: 'medium' }) : 'None'} - ${end ? end.toLocaleDateString('en-US', { dateStyle: 'medium' }) : 'Pending'}`;
  });

  protected onDateSelect(day: CalendarDay): void {
    const selected = day.date;

    if (!this.startDate() || this.activeSelection() === 'start') {
      this.startDate.set(selected);
      this.endDate.set(null);
      this.activeSelection.set('end');
      return;
    }

    const start = this.startDate()!;
    if (selected < start) {
      this.endDate.set(start);
      this.startDate.set(selected);
    } else {
      this.endDate.set(selected);
    }

    this.hoveredDate.set(null);
    this.activeSelection.set('start');
  }

  protected onDateHover(day: CalendarDay): void {
    if (
      !day.isDisabled &&
      this.startDate() &&
      !this.endDate() &&
      this.activeSelection() === 'end'
    ) {
      this.hoveredDate.set(day.date);
    }
  }

  protected onDateLeave(): void {
    this.hoveredDate.set(null);
  }

  protected isDayInHoverRange(day: CalendarDay): boolean {
    const start = this.startDate();
    const hovered = this.hoveredDate();
    const end = this.endDate();

    if (!start || !hovered || end) {
      return false;
    }

    const date = day.date.getTime();
    const startTime = start.getTime();
    const hoveredTime = hovered.getTime();
    const min = Math.min(startTime, hoveredTime);
    const max = Math.max(startTime, hoveredTime);

    return date > min && date < max;
  }

  protected shiftMonth(delta: number): void {
    const next = new Date(this.currentMonth());
    next.setMonth(next.getMonth() + delta);
    this.currentMonth.set(next);
  }

  protected reset(): void {
    this.currentMonth.set(new Date(2026, 6, 1));
    this.startDate.set(new Date(2026, 6, 10));
    this.endDate.set(new Date(2026, 6, 14));
    this.hoveredDate.set(null);
    this.activeSelection.set('start');
  }
}
```

## Booking panel composition
```ts
import { Component, computed, signal } from '@angular/core';
import {
  ButtonComponent,
  CalendarComponent,
  CalendarDay,
  MessageBarComponent,
  TagComponent,
} from 'ui';

@Component({
  selector: 'app-calendar-booking-panel-demo',
  standalone: true,
  imports: [ButtonComponent, CalendarComponent, MessageBarComponent, TagComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:minmax(0,22rem) minmax(0,18rem);gap:1rem;align-items:start;max-width:44rem"
    >
      <div
        style="display:flex;flex-direction:column;gap:1rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="display:flex;flex-direction:column;gap:0.25rem">
          <div style="font-size:0.9375rem;font-weight:600">Book a review window</div>
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            A realistic calendar usually sits inside a booking or scheduling flow, not by itself.
          </div>
        </div>

        <ui-calendar
          [currentMonth]="currentMonth()"
          [selectedDate]="selectedDate()"
          [calendarView]="'days'"
          [size]="'medium'"
          [min]="minDate"
          [max]="maxDate"
          (dateSelect)="onDateSelect($event)"
          (previousMonth)="shiftMonth(-1)"
          (nextMonth)="shiftMonth(1)"
        />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.875rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="display:flex;flex-wrap:wrap;gap:0.5rem">
          <ui-tag text="Remote" appearance="filled" variant="info" />
          <ui-tag text="45 min" appearance="subtle" variant="secondary" />
          <ui-tag text="Slots available" appearance="subtle" variant="success" />
        </div>

        <ui-message-bar variant="info" appearance="subtle">
          Review slots are available only on weekdays between May 12 and May 26.
        </ui-message-bar>

        <div style="display:flex;flex-direction:column;gap:0.375rem">
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            Selection
          </div>
          <div style="font-size:0.9375rem;font-weight:600">{{ summary() }}</div>
        </div>

        <div
          style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
        >
          <ui-button type="button" variant="primary">Confirm slot</ui-button>
          <ui-button type="button" appearance="subtle" (click)="reset()">Reset</ui-button>
        </div>
      </div>
    </div>
  `,
})
export class CalendarBookingPanelDemoComponent {
  protected readonly minDate = '2026-05-12';
  protected readonly maxDate = '2026-05-26';
  protected readonly currentMonth = signal(new Date(2026, 4, 1));
  protected readonly selectedDate = signal<Date | null>(new Date(2026, 4, 19));

  protected readonly summary = computed(() =>
    this.selectedDate()
      ? this.selectedDate()!.toLocaleDateString('en-US', {
          weekday: 'long',
          month: 'long',
          day: 'numeric',
        })
      : 'Choose a review slot.',
  );

  protected onDateSelect(day: CalendarDay): void {
    this.selectedDate.set(day.date);
  }

  protected shiftMonth(delta: number): void {
    const next = new Date(this.currentMonth());
    next.setMonth(next.getMonth() + delta);
    this.currentMonth.set(next);
  }

  protected reset(): void {
    this.currentMonth.set(new Date(2026, 4, 1));
    this.selectedDate.set(new Date(2026, 4, 19));
  }
}
```

## Accessibility

### Current semantics
`ui-calendar` exposes interactive day, month, and year cells as buttons, but it does not currently provide a full ARIA grid pattern.

| Element | Current behavior |
| --- | --- |
| previous and next controls | native button semantics |
| month, day, and year cells | button semantics through `ui-button` |
| disabled dates | native disabled button state |

### Keyboard
The component currently relies on standard button focus order rather than a roving-focus calendar grid.

| Key | Action |
| --- | --- |
| `Tab` / `Shift+Tab` | moves across the header controls and visible cells |
| `Enter` / `Space` | activates the focused day, month, or year button |
| browser focus order | determines movement through the visible buttons |

### Labels and announcements
Because the surface is button-based, the surrounding heading or panel label should explain what the calendar is for, such as booking, scheduling, or range selection. If you need stronger screen-reader guidance, add nearby text that announces the current selection and allowed date window.
