---
title: Datepicker
description: A range date picker built on react-day-picker v9 with price annotations, day status colors, hover-range preview, and a responsive layout that adapts between mobile scroll and desktop two-up view.
---

```tsx
'use client';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

const today = new Date();

const makeDateId = (offset: number) => {
  const d = new Date(today);
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
};

export function DatepickerDemo() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <DeviceProvider device="all">
      <Datepicker
        fromMonth={today}
        selected={selected}
        onSelect={(range) => setSelected(range)}
        labels={{
          seeNextMonth: 'See {currentMonth} and {nextMonth}',
          seePreviousMonth: 'See {previousMonth} and {currentMonth}',
        }}
        dates={[
          { id: makeDateId(3), price: '890 €', status: 'green' },
          { id: makeDateId(5), price: '1 200 €', status: 'orange' },
          { id: makeDateId(7), price: '760 €', status: 'outline' },
        ]}
        statusLabels={{
          green: 'Best price',
          orange: 'Last availabilities',
          outline: 'Available',
        }}
      />
    </DeviceProvider>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/datepicker.json
```

Or manually, copy the following code into `/ui/Datepicker.tsx`:

```tsx
'use client';

import type { Locale } from 'date-fns';
import { addMonths, format, formatISO, isAfter, isBefore, isSameMonth } from 'date-fns';
import type { FunctionComponent, TableHTMLAttributes } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type {
  DateLibOptions,
  DateRange,
  DayButtonProps,
  Matcher,
  Modifiers,
  MonthCaptionProps,
} from 'react-day-picker';
import { DayPicker, useDayPicker } from 'react-day-picker';

import { useDevice } from './contexts/devices/Device';
import { Day, type DayDataProps, type DayProps } from './Day';
import { Button } from './buttons/Button';

import './Datepicker.css';

export const getMonthHeadingId = (date: Date) => `month-heading-${format(date, 'yyyy-MM')}`;

const MonthGridWithLabelledBy = ({
  'aria-label': monthHeadingId,
  children,
  ...props
}: TableHTMLAttributes<HTMLTableElement>) => (
  <table {...props} aria-labelledby={monthHeadingId || undefined}>
    {children}
  </table>
);

interface Labels {
  seeNextMonth: string;
  seePreviousMonth: string;
}

interface MonthCaptionComponentProps extends MonthCaptionProps {
  labels: Labels;
  maxMonth?: Date;
  minMonth?: Date;
}

const MonthCaption = ({
  calendarMonth,
  displayIndex,
  labels,
  minMonth,
  maxMonth,
}: MonthCaptionComponentProps) => {
  const { date: displayMonth } = calendarMonth;
  const { months, goToMonth, nextMonth, previousMonth, dayPickerProps } = useDayPicker();
  const numberOfMonths = months.length;
  const locale = dayPickerProps.locale as Locale | undefined;

  const isFirstDisplayedMonth = displayIndex === 0;
  const isLastDisplayedMonth = displayIndex === numberOfMonths - 1;
  const hideNext = numberOfMonths > 1 && (isFirstDisplayedMonth || !isLastDisplayedMonth);
  const hidePrevious = numberOfMonths > 1 && (isLastDisplayedMonth || !isFirstDisplayedMonth);

  const isPreviousDisabled =
    !previousMonth ||
    (minMonth && isBefore(previousMonth, minMonth) && !isSameMonth(previousMonth, minMonth));
  const isNextDisabled =
    !nextMonth || (maxMonth && isAfter(nextMonth, maxMonth) && !isSameMonth(nextMonth, maxMonth));

  const focusMonthHeading = useCallback((date: Date) => {
    requestAnimationFrame(() => {
      document.getElementById(getMonthHeadingId(date))?.focus();
    });
  }, []);

  const handlePrevious = useCallback(() => {
    if (previousMonth) {
      goToMonth(previousMonth);
      focusMonthHeading(previousMonth);
    }
  }, [previousMonth, goToMonth, focusMonthHeading]);

  const handleNext = useCallback(() => {
    if (nextMonth) {
      goToMonth(nextMonth);
      focusMonthHeading(nextMonth);
    }
  }, [nextMonth, goToMonth, focusMonthHeading]);

  const previousMonthLabel = useMemo(() => {
    const actualPreviousMonth =
      isFirstDisplayedMonth && numberOfMonths > 1 ? addMonths(displayMonth, -1) : previousMonth;
    const previousMonthLabel = actualPreviousMonth ? format(actualPreviousMonth, 'MMMM yyy') : '';
    const currentMonthLabel = displayMonth ? format(displayMonth, 'MMMM yyy') : '';
    return labels.seePreviousMonth
      .replace('{previousMonth}', previousMonthLabel)
      .replace('{currentMonth}', currentMonthLabel);
  }, [displayMonth, isFirstDisplayedMonth, labels.seePreviousMonth, numberOfMonths, previousMonth]);

  const nextMonthLabel = useMemo(() => {
    const currentMonthLabel = displayMonth ? format(displayMonth, 'MMMM yyy') : '';
    const actualNextMonth =
      isLastDisplayedMonth && numberOfMonths > 1 ? addMonths(displayMonth, 1) : nextMonth;
    const nextMonthLabel = actualNextMonth ? format(actualNextMonth, 'MMMM yyy') : '';
    return labels.seeNextMonth
      .replace('{currentMonth}', currentMonthLabel)
      .replace('{nextMonth}', nextMonthLabel);
  }, [displayMonth, labels, nextMonth, isLastDisplayedMonth, numberOfMonths]);

  return (
    <div className="text-b2 relative isolate flex h-48 items-center justify-center px-56 font-semibold capitalize">
      {!hidePrevious && (
        <Button
          aria-label={previousMonthLabel}
          className="absolute start-0 hidden outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 md:inline-block"
          color="black"
          icon="ArrowDefaultLeft"
          disabled={!!isPreviousDisabled}
          onClick={handlePrevious}
          variant="circle"
        />
      )}
      <p role="heading" aria-level={3} tabIndex={-1} id={getMonthHeadingId(displayMonth)}>
        {format(displayMonth, 'MMM yyy', { locale })}
      </p>
      {!hideNext && (
        <Button
          aria-label={nextMonthLabel}
          className="absolute end-0 hidden outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 md:inline-block"
          color="black"
          icon="ArrowDefaultRight"
          disabled={!!isNextDisabled}
          onClick={handleNext}
          variant="circle"
        />
      )}
    </div>
  );
};

const formatWeekdayName = (weekday: Date, options?: DateLibOptions) =>
  format(weekday, 'cccccc', options);

type StatusLabelMap = Partial<Record<'green' | 'orange' | 'outline', string>>;

type SelectRangeHandler = (
  selected: DateRange | undefined,
  triggerDate: Date,
  modifiers: Modifiers,
  e: React.MouseEvent | React.KeyboardEvent,
) => void;

export interface DatepickerProps {
  dates?: DayDataProps[];
  defaultMonth?: Date;
  disabledDays?: Matcher | Matcher[];
  formatDurationsLabel?: (count: number) => string;
  fromMonth?: Date;
  labels: Labels;
  locale?: Locale;
  maxNumberOfMonths?: number;
  onSelect?: SelectRangeHandler;
  selected?: DateRange;
  statusLabels?: StatusLabelMap;
}

export const Datepicker: FunctionComponent<DatepickerProps> = ({
  dates = [],
  defaultMonth,
  disabledDays,
  formatDurationsLabel,
  fromMonth,
  labels,
  locale,
  maxNumberOfMonths = 12,
  onSelect,
  selected,
  statusLabels,
}) => {
  const isDesktop = useDevice('medium');
  const [hoveredDate, setHoveredDate] = useState<Date | undefined>();

  const modifiers = useMemo(() => {
    const modifiersObj: Record<string, (date: Date) => boolean> = {};

    if (!hoveredDate || !isDesktop) {
      return modifiersObj;
    }

    const getIsMiddleRange = (date: Date) => {
      const startDate = selected?.from ?? selected?.to;
      if (!startDate) return false;
      return isAfter(date, startDate) && isBefore(date, hoveredDate);
    };

    if ((selected?.from && !selected.to) || (selected?.to && !selected.from)) {
      modifiersObj.hover_range_middle = (date: Date) => getIsMiddleRange(date);
    }

    if (selected?.to && !selected?.from) {
      modifiersObj.hover_range_end = (date: Date) => {
        const isMiddle = getIsMiddleRange(date);
        return !isMiddle && isAfter(date, selected.to!);
      };
    }

    if (selected?.from && !selected?.to) {
      modifiersObj.hover_range_end = (date: Date) => {
        const isMiddle = getIsMiddleRange(date);
        return !isMiddle && isAfter(date, selected.from!);
      };
    }

    return modifiersObj;
  }, [hoveredDate, isDesktop, selected?.from, selected?.to]);

  const minMonth = useMemo(() => fromMonth ?? new Date(), [fromMonth]);

  const maxMonth = useMemo(() => {
    const end = new Date(minMonth);
    end.setMonth(end.getMonth() + maxNumberOfMonths);
    return end;
  }, [minMonth, maxNumberOfMonths]);

  const MonthCaptionWithBounds = useCallback(
    (props: MonthCaptionProps) => (
      <MonthCaption {...props} labels={labels} minMonth={minMonth} maxMonth={maxMonth} />
    ),
    [labels, minMonth, maxMonth],
  );

  const components = useMemo(
    () => ({
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      DayButton: ({ 'aria-label': ariaLabel, ...props }: DayButtonProps) => {
        const date = formatISO(props.day.date, { representation: 'date' });
        const dayData = dates.find(({ id }) => id === date);
        const statusLabel =
          dayData?.status && dayData.status !== 'default'
            ? statusLabels?.[dayData.status as 'green' | 'orange' | 'outline']
            : undefined;
        return (
          <Day
            data-name="Datepicker-Day"
            formatDurationsLabel={formatDurationsLabel}
            label={statusLabel}
            {...(props as DayButtonProps & DayProps)}
            {...dayData}
          />
        );
      },
      MonthCaption: MonthCaptionWithBounds,
      MonthGrid: MonthGridWithLabelledBy,
    }),
    [dates, formatDurationsLabel, MonthCaptionWithBounds, statusLabels],
  );

  const handleDayMouseEnter = useCallback(
    (date: Date) => {
      if (!isDesktop || hoveredDate?.getTime() === date.getTime()) return;
      setHoveredDate(date);
    },
    [isDesktop, hoveredDate],
  );

  const handleDayMouseLeave = useCallback(() => setHoveredDate(undefined), []);

  const handleSelect = useCallback<SelectRangeHandler>(
    (newRange, triggerDate, mods, e) => {
      if (!onSelect) return;
      if (
        newRange?.from &&
        newRange?.to &&
        newRange.from.getTime() === newRange.to.getTime() &&
        !selected?.from &&
        !selected?.to
      ) {
        onSelect({ from: newRange.from, to: undefined }, triggerDate, mods, e);
        return;
      }
      onSelect(newRange, triggerDate, mods, e);
    },
    [onSelect, selected?.from, selected?.to],
  );

  const effectiveDefaultMonth = useMemo(() => {
    if (isDesktop) return defaultMonth ?? minMonth;
    return undefined;
  }, [isDesktop, defaultMonth, minMonth]);

  const dayPickerLabels = useMemo(
    () => ({ labelGrid: (date: Date) => getMonthHeadingId(date) }),
    [],
  );

  return (
    <DayPicker
      components={components}
      defaultMonth={effectiveDefaultMonth}
      disabled={disabledDays}
      formatters={{ formatWeekdayName }}
      labels={dayPickerLabels}
      locale={locale}
      mode="range"
      hideNavigation
      numberOfMonths={isDesktop ? 2 : maxNumberOfMonths}
      onSelect={handleSelect}
      selected={selected}
      modifiers={modifiers}
      modifiersClassNames={{
        hover_range_middle: 'rdp-range_middle',
        hover_range_end: 'rdp-range_end',
      }}
      onDayMouseEnter={handleDayMouseEnter}
      onDayMouseLeave={handleDayMouseLeave}
      classNames={{ day: 'group rdp-day' }}
    />
  );
};
```

## Dependencies

Install the peer dependencies:

```bash
npm install react-day-picker date-fns
```

The component reads the current breakpoint via `useDevice`, which requires `DeviceProvider` somewhere above it in the tree. In most apps this belongs at the root layout:

```tsx
// app/layout.tsx (Next.js) or your root provider
import { DeviceProvider } from '@/ui/contexts/devices/Device';

export default function RootLayout({ children }) {
  return (
    <DeviceProvider device="all">
      {children}
    </DeviceProvider>
  );
}
```

You can also wrap just the Datepicker when `DeviceProvider` is not at the root:

```tsx
import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

<DeviceProvider device="all">
  <Datepicker ... />
</DeviceProvider>
```

## Usage

### Basic range selection

The minimum configuration: controlled state, a `fromMonth` to anchor the calendar, and the two navigation button labels.

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import type { DateRange } from 'react-day-picker';
import { Datepicker } from '@/ui/Datepicker';

export function Component() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <Datepicker
      fromMonth={new Date()}
      selected={selected}
      onSelect={(range) => setSelected(range)}
      labels={{
        seeNextMonth: 'See {currentMonth} and {nextMonth}',
        seePreviousMonth: 'See {previousMonth} and {currentMonth}',
      }}
    />
  );
}
```

### With a pre-selected range

Pass an initial `selected` value to open the calendar with a range already highlighted.

```tsx
'use client';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

// Build a date offset from today
const d = (offsetDays: number) => {
  const date = new Date();
  date.setDate(date.getDate() + offsetDays);
  return date;
};

const id = (offsetDays: number) => d(offsetDays).toISOString().slice(0, 10);

export function DatepickerPreselectedDemo() {
  const [selected, setSelected] = useState<DateRange>({
    from: d(5),
    to: d(12),
  });

  return (
    <DeviceProvider device="all">
      <Datepicker
        fromMonth={new Date()}
        selected={selected}
        onSelect={(range) => setSelected(range ?? { from: undefined, to: undefined })}
        labels={{
          seeNextMonth: 'See {currentMonth} and {nextMonth}',
          seePreviousMonth: 'See {previousMonth} and {currentMonth}',
        }}
        dates={[
          { id: id(3), price: '890 €', status: 'green' },
          { id: id(4), price: '910 €', status: 'green' },
          { id: id(8), price: '1 450 €', status: 'orange' },
          { id: id(9), price: '1 450 €', status: 'orange' },
          { id: id(14), price: '720 €', status: 'outline' },
          { id: id(15), price: '720 €', status: 'outline' },
          { id: id(20), price: '1 100 €', status: 'default' },
        ]}
        statusLabels={{
          green: 'Best price of the month',
          orange: 'Last available dates',
          outline: 'Available',
        }}
      />
    </DeviceProvider>
  );
}
```

### With price and status annotations

Each day can carry a price string and a status that controls its visual style. Pass the `dates` array and provide `statusLabels` for screen-reader announcements.

```tsx showLineNumbers
<Datepicker
  fromMonth={new Date()}
  selected={selected}
  onSelect={setSelected}
  labels={{ ... }}
  dates={[
    { id: '2025-07-10', price: '890 €',   status: 'green'   },
    { id: '2025-07-11', price: '890 €',   status: 'green'   },
    { id: '2025-07-15', price: '1 450 €', status: 'orange'  },
    { id: '2025-07-20', price: '720 €',   status: 'outline' },
    { id: '2025-07-22', price: '1 100 €', status: 'default' },
  ]}
  statusLabels={{
    green:   'Best price of the month',
    orange:  'Last available dates',
    outline: 'Available',
  }}
/>
```

### With disabled days

Pass any react-day-picker `Matcher` or array of `Matcher` values to `disabledDays`. Common patterns include blocking dates before today, specific dates, and date ranges.

```tsx
'use client';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

const d = (offsetDays: number) => {
  const date = new Date();
  date.setDate(date.getDate() + offsetDays);
  return date;
};

export function DatepickerDisabledDaysDemo() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <DeviceProvider device="all">
      <Datepicker
        fromMonth={new Date()}
        selected={selected}
        onSelect={(range) => setSelected(range)}
        disabledDays={[
          // Prevent selecting past dates
          { before: new Date() },
          // Block specific unavailable dates
          d(4),
          d(5),
          d(11),
          d(12),
          d(18),
          d(19),
          // Block out an entire period
          { from: d(25), to: d(30) },
        ]}
        labels={{
          seeNextMonth: 'See {currentMonth} and {nextMonth}',
          seePreviousMonth: 'See {previousMonth} and {currentMonth}',
        }}
      />
    </DeviceProvider>
  );
}
```

### With duration tooltip

When the user has selected a start date and hovers over a potential end date, a tooltip shows how many nights that range would be. Provide a `formatDurationsLabel` function that receives the night count and returns the string to display.

```tsx
'use client';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

const d = (offsetDays: number) => {
  const date = new Date();
  date.setDate(date.getDate() + offsetDays);
  return date;
};

const id = (offsetDays: number) => d(offsetDays).toISOString().slice(0, 10);

const pluralNights = (n: number) => (n === 1 ? '1 night' : `${n} nights`);

export function DatepickerDurationTooltipDemo() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <DeviceProvider device="all">
      <Datepicker
        fromMonth={new Date()}
        selected={selected}
        onSelect={(range) => setSelected(range)}
        formatDurationsLabel={pluralNights}
        labels={{
          seeNextMonth: 'See {currentMonth} and {nextMonth}',
          seePreviousMonth: 'See {previousMonth} and {currentMonth}',
        }}
        dates={[
          { id: id(2), price: '1 090 €', status: 'green' },
          { id: id(3), price: '1 090 €', status: 'green' },
          { id: id(6), price: '1 480 €', status: 'orange' },
          { id: id(7), price: '1 480 €', status: 'orange' },
          { id: id(10), price: '850 €', status: 'outline' },
          { id: id(13), price: '920 €', status: 'default' },
        ]}
        statusLabels={{
          green: 'Best price',
          orange: 'Last availabilities',
          outline: 'Available',
        }}
        disabledDays={{ before: new Date() }}
      />
    </DeviceProvider>
  );
}
```

The tooltip appears above the hovered end date on desktop only (mouse events are not fired on touch).

### Without prices (simple mode)

Omitting `dates` gives a clean calendar with no sub-labels. Useful when availability is expressed through `disabledDays` alone.

```tsx
'use client';
import { useState } from 'react';
import type { DateRange } from 'react-day-picker';

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { Datepicker } from '@/ui/Datepicker';

const d = (offsetDays: number) => {
  const date = new Date();
  date.setDate(date.getDate() + offsetDays);
  return date;
};

export function DatepickerNoPricesDemo() {
  const [selected, setSelected] = useState<DateRange | undefined>();

  return (
    <div className="space-y-16">
      <DeviceProvider device="all">
        <Datepicker
          fromMonth={new Date()}
          selected={selected}
          onSelect={(range) => setSelected(range)}
          disabledDays={[{ before: new Date() }, d(6), d(7), d(13), d(14)]}
          labels={{
            seeNextMonth: 'See {currentMonth} and {nextMonth}',
            seePreviousMonth: 'See {previousMonth} and {currentMonth}',
          }}
        />
      </DeviceProvider>
      {selected?.from && (
        <p className="text-b3 text-center">
          {selected.from.toLocaleDateString()}
          {selected.to ? ` → ${selected.to.toLocaleDateString()}` : ' → pick an end date'}
        </p>
      )}
    </div>
  );
}
```

## API Reference

### Datepicker

| Prop                   | Type                        | Default      | Description                                                                                              |
| ---------------------- | --------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
| `labels`               | `Labels`                    | —            | **Required.** Accessible labels for the previous/next month navigation buttons. Supports token interpolation — see [Labels](#labels). |
| `selected`             | `DateRange`                 | `undefined`  | The current selection as `{ from?: Date; to?: Date }`. The component is fully controlled: you own the state. |
| `onSelect`             | `SelectRangeHandler`        | `undefined`  | Fired every time the user clicks a date. Receives `(range, triggerDate, modifiers, event)`. `range` may be `undefined` if the user deselects. |
| `dates`                | `DayDataProps[]`            | `[]`         | Per-day data objects that annotate individual days with a price and a status color. See [DayDataProps](#daydataprops). |
| `statusLabels`         | `StatusLabelMap`            | `undefined`  | Screen-reader text for each non-default status. If a day has `status: 'green'` and `statusLabels.green` is set, a visually-hidden `<span>` with that text is rendered inside the day button. |
| `disabledDays`         | `Matcher \| Matcher[]`      | `undefined`  | Days that cannot be selected. Accepts any react-day-picker `Matcher` — a `Date`, a `{ before }` / `{ after }` / `{ from, to }` object, or an array mixing any of the above. |
| `fromMonth`            | `Date`                      | `new Date()` | The earliest month the user can navigate to. Also the default starting month on desktop. The previous-month button is disabled when the displayed month equals this value. |
| `defaultMonth`         | `Date`                      | `fromMonth`  | The month shown initially on desktop. Has no effect on mobile (the full scroll list always starts at `fromMonth`). |
| `maxNumberOfMonths`    | `number`                    | `12`         | How many months to render on mobile. The scroll list always starts at `fromMonth` and ends `maxNumberOfMonths` months later. |
| `locale`               | `Locale`                    | `undefined`  | A [date-fns locale](https://date-fns.org/docs/I18n) object. Affects weekday names, month names, and date formatting in navigation labels. |
| `formatDurationsLabel` | `(nights: number) => string`| `undefined`  | When provided, a tooltip appears above the hovered end date showing this label. Only fires on desktop (hover events). The function receives the integer night count. |

### Labels

Navigation button labels support token interpolation. The tokens are replaced with locale-formatted month names at render time.

| Field              | Tokens available                      | Example value                                      |
| ------------------ | ------------------------------------- | -------------------------------------------------- |
| `seeNextMonth`     | `{currentMonth}`, `{nextMonth}`       | `"See {currentMonth} and {nextMonth}"`             |
| `seePreviousMonth` | `{previousMonth}`, `{currentMonth}`   | `"See {previousMonth} and {currentMonth}"`         |

The resulting string becomes the `aria-label` on the navigation button, which is read aloud by screen readers. Choose a phrasing that is meaningful as a spoken sentence, e.g. _"See January 2026 and February 2026"_.

On mobile the buttons are always hidden visually (`hidden md:inline-block`) but remain in the DOM with their labels so keyboard users can still navigate.

### DayDataProps

Annotates a single calendar day. Pass an array of these objects to the `dates` prop.

| Field    | Type                                               | Description                                                                                                                                                    |
| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`     | `string`                                           | **Required.** The date to annotate, in ISO `yyyy-MM-dd` format (e.g. `"2025-07-15"`). Days not present in the array render with the default unstyled appearance. |
| `price`  | `string \| null`                                   | A pre-formatted price string displayed below the day number (e.g. `"1 200 €"`). Hidden when the day is part of a selected range. Pass `null` to explicitly show no price. |
| `status` | `"default" \| "green" \| "orange" \| "outline"`   | Controls the background pill drawn behind the day number. See [Day statuses](#day-statuses) for the visual mapping. |

### StatusLabelMap

Maps each non-default status value to a screen-reader string. Only the statuses you include are announced.

```ts
interface StatusLabelMap {
  green?:   string;   // e.g. "Best price of the month"
  orange?:  string;   // e.g. "Last available dates"
  outline?: string;   // e.g. "Available"
}
```

### SelectRangeHandler

The full type of the `onSelect` callback:

```ts
type SelectRangeHandler = (
  selected: DateRange | undefined,
  triggerDate: Date,
  modifiers: Modifiers,
  e: React.MouseEvent | React.KeyboardEvent,
) => void;
```

- **`selected`** — the new range after the click. `undefined` when the user deselects the last date. The component normalises the first click to `{ from: date, to: undefined }` to match v8 behaviour (react-day-picker v9 sets `from === to` on the first click internally).
- **`triggerDate`** — the exact date the user clicked.
- **`modifiers`** — the full react-day-picker modifier state for `triggerDate` (e.g. `selected`, `disabled`, `today`).
- **`e`** — the original mouse or keyboard event.

## Day statuses

Each status draws a different background on the day-number circle:

| Status    | Visual                                              | When to use                                        |
| --------- | --------------------------------------------------- | -------------------------------------------------- |
| `default` | No background — plain number                        | Date is available but has no special highlight     |
| `green`   | Filled green pill (`bg-green text-white`)           | Best-price day of the month                        |
| `orange`  | Filled orange rounded square (`bg-orange text-white`) | High-demand / last available dates               |
| `outline` | White rounded square with black border              | Available but not the cheapest or most constrained |

When a day is **disabled**, all statuses are overridden with `bg-sand text-middleGrey opacity-60`.

When a day is **selected** (part of the highlighted range), the status background is suppressed — the selection highlight from the calendar CSS takes over.

## Responsive behaviour

The component switches layout based on the `medium` breakpoint provided by `DeviceProvider`:

| Viewport   | Layout                                      | Navigation                                |
| ---------- | ------------------------------------------- | ----------------------------------------- |
| Mobile     | Vertical scroll list of `maxNumberOfMonths` months | Previous/next buttons hidden visually, but focusable |
| Desktop    | Two months side by side                     | Previous/next buttons fully visible       |

The `medium` breakpoint corresponds to `min-width: 768px` in the default Trident UI theme. `DeviceProvider` reads this via `matchMedia`, so the layout responds to window resize without a page reload.

## Hover-range preview

On desktop, when the user has selected exactly one date (either `from` or `to` is set but not both), hovering over a candidate end date highlights the in-between days using the same `rdp-range_middle` class as the confirmed selection. This gives the user a visual preview of the range before committing.

The preview is implemented with custom day modifiers (`hover_range_middle`, `hover_range_end`) that are mapped to the CSS class names from the confirmed range. It is completely suppressed on mobile because `mouseover` events do not fire on touch screens.

## CSS

The component ships its own stylesheet (`Datepicker.css`) imported at the top of `Datepicker.tsx`. It targets the `rdp-*` class names emitted by react-day-picker v9 and overrides them with Trident UI design tokens:

```
.rdp-root           — container background (lightSand), border-radius, padding
.rdp-months         — flex column on mobile, row on md+
.rdp-day_button     — invisible border box that shows on focus / hover
.rdp-range_middle   — range fill (white bg + black text)
.rdp-range_start    — start-of-range rounded start corner
.rdp-range_end      — end-of-range rounded end corner
.rdp-day[data-selected] — selected day (black bg, white text)
.rdp-day[data-disabled] — reduced opacity
```

The file uses `@reference` to pull in the global CSS variables, so Tailwind utility classes (`@apply`) resolve correctly. If you copy the component manually, make sure your build pipeline processes this file with the same Tailwind config.

## Accessibility

- **Month headings** are rendered as `<p role="heading" aria-level="3" tabindex="-1">`. After the user clicks a navigation button, focus moves programmatically to the heading of the newly visible first month so keyboard and screen-reader users receive immediate feedback.
- **Month grids** (`<table>`) are annotated with `aria-labelledby` pointing to their month heading's `id`, so screen readers can announce which month's grid the user is navigating.
- **Navigation buttons** carry full spoken labels produced by interpolating the `Labels` tokens. On mobile the buttons are `hidden md:inline-block` but remain in the tab order.
- **Day status labels** are injected as visually-hidden `<span class="sr-only">` text inside each annotated day button. Screen readers will read e.g. _"15, Best price of the month"_ rather than just _"15"_.
- **Disabled days** receive the standard `disabled` HTML attribute on their button, which prevents focus and interaction without relying on visual styling alone.

## Notes

### Controlled-only

The Datepicker is fully controlled — it holds no internal selection state. You must always pass `selected` and `onSelect` together, otherwise clicking a date has no visible effect.

### First-click normalisation

react-day-picker v9 sets `from === to` when the user clicks the very first date (where previously v8 only set `from`). The component normalises this by detecting the case and calling `onSelect` with `{ from: date, to: undefined }` instead. This preserves the v8 interaction model without requiring consumer code changes.

### `fromMonth` is the lower bound for navigation, not for selection

`fromMonth` prevents the user from navigating to an earlier month, but it does not disable the dates within that month. If you also want to disable dates before today, pass `{ before: new Date() }` (or your custom lower bound) to `disabledDays`.

### Price string formatting

The `price` field in `DayDataProps` is a pre-formatted display string — the component does not apply any currency logic. Format the string on the data side before passing it in (e.g. `Intl.NumberFormat` or a backend-provided string). Very long price strings are scaled down automatically using a CSS `scale` transform based on character count, so they do not overflow the day cell.

### Client component

The Datepicker uses `useState`, `useCallback`, and `useMemo`. Add `'use client'` at the top of any file that renders it in a Next.js App Router project.
