---
title: CriteriaDates
description: A Datepicker wrapper for booking search flows, with an optional price-status legend, section card chrome, and a child slot for reinsurance messages.
---

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

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { CriteriaDates, CriteriaDatesReinsurance } from '@/ui/criteria/CriteriaDates';

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

  const labels = {
    seeNextMonth: 'Next month',
    seePreviousMonth: 'Previous month',
    bestPrice: 'Best price',
    lastAvailabilities: 'Last availabilities',
    notAvailable: 'Not available',
    priceLegend: 'Price legend',
  };

  return (
    <DeviceProvider device="all">
      <div className="w-[340px] space-y-24">
        <CriteriaDates
          labels={labels}
          showPriceLegend
          selected={selected}
          onSelect={setSelected}
          title="Select your dates"
        >
          <CriteriaDatesReinsurance
            labels={{ disclaimer: 'New! Flexible booking now available.' }}
          />
        </CriteriaDates>
        {selected?.from && (
          <p className="text-b4">
            From: {selected.from.toLocaleDateString()}
            {selected.to ? ` — To: ${selected.to.toLocaleDateString()}` : ''}
          </p>
        )}
      </div>
    </DeviceProvider>
  );
}
```

## Installation

Using the CLI:

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

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

```tsx
'use client';
import clsx from 'clsx';
import type { ComponentProps, FunctionComponent, PropsWithChildren } from 'react';

import { Chip } from '../Chip';
import { Datepicker } from '../Datepicker';
import { CriteriaSection } from './CriteriaSection';

export interface CriteriaDatesReinsuranceProps {
  className?: string;
  labels: { disclaimer: string };
}

export const CriteriaDatesReinsurance: FunctionComponent<CriteriaDatesReinsuranceProps> = ({
  className,
  labels,
}) => {
  if (!labels.disclaimer) return null;

  return (
    <p
      className={clsx(
        'text-b3 flex items-start justify-center gap-20 text-start text-black md:items-center md:gap-8',
        className,
      )}
    >
      <Chip color="sienna" icon="New" size="size-32" theme="solid" />
      {labels.disclaimer}
    </p>
  );
};

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

interface PriceLegendLabels {
  bestPrice: string;
  lastAvailabilities: string;
  notAvailable: string;
  priceLegend: string;
  availableSeason?: string;
}

export interface CriteriaDatesLabels extends DatepickerLabels {
  availableSeason?: string;
}

export type CriteriaDatesLabelsWithPriceLegend = CriteriaDatesLabels & PriceLegendLabels;

export interface CriteriaDatesProps extends ComponentProps<typeof Datepicker> {
  className?: string;
  labels: CriteriaDatesLabels | CriteriaDatesLabelsWithPriceLegend;
  showPriceLegend?: boolean;
  title?: string;
}

const LegendItem: FunctionComponent<{ customClass: string; label: string }> = ({
  customClass,
  label,
}) => (
  <p className="flex items-center gap-4" role="listitem">
    <span className={clsx('size-27', customClass)} />
    {label}
  </p>
);

export const CriteriaDates: FunctionComponent<PropsWithChildren<CriteriaDatesProps>> = ({
  className,
  showPriceLegend,
  labels,
  children,
  title,
  ...datepickerProps
}) => {
  const hasPriceLegend = showPriceLegend && 'priceLegend' in labels;

  const statusLabels = hasPriceLegend
    ? {
        green: labels.bestPrice,
        orange: labels.lastAvailabilities,
        outline: labels.availableSeason,
      }
    : undefined;

  const priceLegend = hasPriceLegend && (
    <div
      aria-labelledby="price-legend-title"
      className="text-b4 flex flex-wrap items-center justify-center gap-x-20 gap-y-12"
      role="region"
    >
      <p className="w-full text-center font-semibold md:w-auto" id="price-legend-title">
        {labels.priceLegend}
      </p>
      <div
        aria-label={labels.priceLegend}
        className="flex items-center justify-center gap-12"
        role="list"
      >
        <LegendItem customClass="bg-green rounded-pill" label={labels.bestPrice} />
        <LegendItem customClass="bg-orange rounded-8" label={labels.lastAvailabilities} />
        <LegendItem customClass="bg-sand rounded-8 diagonal-line" label={labels.notAvailable} />
        {labels.availableSeason && (
          <LegendItem customClass="rounded-8 border border-black" label={labels.availableSeason} />
        )}
      </div>
    </div>
  );

  return (
    <CriteriaSection className={clsx('!gap-y-12 md:!gap-y-20', className)} title={title}>
      {priceLegend}
      <div className="flex flex-col">
        <Datepicker
          {...datepickerProps}
          labels={{
            seeNextMonth: labels.seeNextMonth,
            seePreviousMonth: labels.seePreviousMonth,
          }}
          statusLabels={statusLabels}
        />
        {children}
      </div>
    </CriteriaSection>
  );
};
```

## Dependencies

`CriteriaDates` wraps `Datepicker` internally, which reads the current breakpoint via `useDevice`. Place `DeviceProvider` above it in the tree:

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

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

Install the peer dependencies used by `Datepicker`:

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

See the [Datepicker](/docs/components/datepicker#dependencies) documentation for the full `DeviceProvider` setup guide.

## Usage

### Basic range selection

`CriteriaDates` is controlled the same way as `Datepicker` — you own the `DateRange` state and pass it back via `onSelect`. The `labels` object must always contain `seeNextMonth` and `seePreviousMonth` for the accessible navigation button labels.

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import type { DateRange } from 'react-day-picker';
import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { CriteriaDates } from '@/ui/criteria/CriteriaDates';

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

  return (
    <DeviceProvider device="all">
      <CriteriaDates
        fromMonth={new Date()}
        labels={{
          seeNextMonth: 'Next month',
          seePreviousMonth: 'Previous month',
        }}
        selected={selected}
        onSelect={setSelected}
        title="Select your dates"
      />
    </DeviceProvider>
  );
}
```

### With price legend

Pass `showPriceLegend` and include the extra legend keys in `labels` to render a colour-coded legend above the calendar. The type system enforces that the extra keys must be present when the legend is enabled — TypeScript will raise an error if `showPriceLegend` is `true` but the price keys are missing from `labels`.

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

import { DeviceProvider } from '@/ui/contexts/devices/Device';
import { CriteriaDates, CriteriaDatesReinsurance } from '@/ui/criteria/CriteriaDates';

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

  const labels = {
    seeNextMonth: 'Next month',
    seePreviousMonth: 'Previous month',
    bestPrice: 'Best price',
    lastAvailabilities: 'Last availabilities',
    notAvailable: 'Not available',
    priceLegend: 'Price legend',
  };

  return (
    <DeviceProvider device="all">
      <div className="w-[340px] space-y-24">
        <CriteriaDates
          labels={labels}
          showPriceLegend
          selected={selected}
          onSelect={setSelected}
          title="Select your dates"
        >
          <CriteriaDatesReinsurance
            labels={{ disclaimer: 'New! Flexible booking now available.' }}
          />
        </CriteriaDates>
        {selected?.from && (
          <p className="text-b4">
            From: {selected.from.toLocaleDateString()}
            {selected.to ? ` — To: ${selected.to.toLocaleDateString()}` : ''}
          </p>
        )}
      </div>
    </DeviceProvider>
  );
}
```

### With reinsurance message

Place `<CriteriaDatesReinsurance>` as a child of `<CriteriaDates>` to render a disclaimer below the calendar. The block renders nothing when `labels.disclaimer` is an empty string, so you can conditionally suppress it by toggling the string value.

```tsx showLineNumbers
import { CriteriaDates, CriteriaDatesReinsurance } from '@/ui/criteria/CriteriaDates';

<CriteriaDates
  fromMonth={new Date()}
  labels={{ seeNextMonth: 'Next month', seePreviousMonth: 'Previous month' }}
  selected={selected}
  onSelect={setSelected}
>
  <CriteriaDatesReinsurance
    labels={{ disclaimer: 'New! Flexible booking now available on all resorts.' }}
  />
</CriteriaDates>
```

### With date annotations and disabled days

All `Datepicker` props are forwarded unchanged, including `dates` for per-day price and status annotations and `disabledDays` for blocking unavailable dates. When `showPriceLegend` is enabled, `statusLabels` are derived automatically from the legend text — screen readers will announce the status of each annotated day without any extra configuration.

```tsx showLineNumbers
<CriteriaDates
  fromMonth={new Date()}
  labels={{
    seeNextMonth: 'Next month',
    seePreviousMonth: 'Previous month',
    bestPrice: 'Best price',
    lastAvailabilities: 'Last availabilities',
    notAvailable: 'Not available',
    priceLegend: 'Price legend',
  }}
  showPriceLegend
  selected={selected}
  onSelect={setSelected}
  disabledDays={{ before: new Date() }}
  dates={[
    { id: '2025-08-10', price: '890 €',   status: 'green'   },
    { id: '2025-08-15', price: '1 450 €', status: 'orange'  },
    { id: '2025-08-20', price: '720 €',   status: 'outline' },
  ]}
/>
```

## API Reference

### CriteriaDates

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `labels` | `CriteriaDatesLabels \| CriteriaDatesLabelsWithPriceLegend` | — | **Required.** Text labels for navigation buttons and, when `showPriceLegend` is `true`, the legend items. See [CriteriaDatesLabels](#criteriaDatesLabels) and [CriteriaDatesLabelsWithPriceLegend](#criteriaDatesLabelsWithPriceLegend). |
| `showPriceLegend` | `boolean` | `false` | When `true`, renders the colour-coded price legend above the calendar. Requires `priceLegend`, `bestPrice`, `lastAvailabilities`, and `notAvailable` to be present in `labels`. |
| `title` | `string` | `undefined` | Optional heading displayed at the top of the section card. |
| `className` | `string` | `undefined` | Additional CSS class on the section card wrapper. |
| `children` | `ReactNode` | `undefined` | Slot rendered below the calendar. Intended for `CriteriaDatesReinsurance`. |
| `...datepickerProps` | `DatepickerProps` | — | All remaining props are forwarded to the inner `Datepicker` — `selected`, `onSelect`, `fromMonth`, `defaultMonth`, `maxNumberOfMonths`, `dates`, `disabledDays`, `locale`, `formatDurationsLabel`. |

### CriteriaDatesLabels

Base labels required for all configurations.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `seeNextMonth` | `string` | Accessible label for the next-month navigation button. Supports `{currentMonth}` and `{nextMonth}` token interpolation. |
| `seePreviousMonth` | `string` | Accessible label for the previous-month navigation button. Supports `{previousMonth}` and `{currentMonth}` token interpolation. |
| `availableSeason` | `string?` | Optional label for the season-availability legend item (outline square). Omit to hide the swatch. |

### CriteriaDatesLabelsWithPriceLegend

Extends `CriteriaDatesLabels` with the following fields. Must be provided when `showPriceLegend` is `true`.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `priceLegend` | `string` | Title of the legend region; also its `aria-label`. |
| `bestPrice` | `string` | Label for the green (best price) swatch. |
| `lastAvailabilities` | `string` | Label for the orange (high-demand) swatch. |
| `notAvailable` | `string` | Label for the diagonal-strikethrough (unavailable) swatch. |
| `availableSeason` | `string?` | Inherited from `CriteriaDatesLabels`. |

### CriteriaDatesReinsurance

A companion component rendered as a child of `CriteriaDates`.

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `labels.disclaimer` | `string` | — | **Required.** The disclaimer text. The component renders nothing when this string is empty. |
| `className` | `string` | `undefined` | Additional CSS class on the paragraph element. |

## Price legend

When `showPriceLegend` is enabled a legend is rendered above the calendar with colour-coded swatches that mirror the `Datepicker` day statuses:

| Swatch | Day status | Meaning |
| ------ | ---------- | ------- |
| Green pill (`bg-green`) | `green` | Best price of the period |
| Orange rounded square (`bg-orange`) | `orange` | High-demand / last available dates |
| Strikethrough square (`diagonal-line`) | Disabled day | Date not available |
| Outline square (white + black border) | `outline` | Season availability (optional) |

The `statusLabels` passed to the inner `Datepicker` are derived automatically from the legend labels — no extra configuration needed for screen-reader announcements.

## Notes

### Labels union type

The `labels` prop is a union of `CriteriaDatesLabels` and `CriteriaDatesLabelsWithPriceLegend`. TypeScript raises a type error if you pass `showPriceLegend={true}` without the legend keys. Provide the full object to keep the type narrow, even if you conditionally hide the legend at runtime.

### All Datepicker props are forwarded

`CriteriaDates` spreads any prop it does not recognise directly onto the inner `Datepicker`. `labels.seeNextMonth` and `labels.seePreviousMonth` are extracted and passed to `Datepicker`'s own `labels` prop automatically — do not duplicate them.

### Controlled-only

Like the underlying `Datepicker`, `CriteriaDates` holds no selection state. Always pass both `selected` and `onSelect`, otherwise clicks have no visible effect.

### Client component

`CriteriaDates` uses React hooks internally. Add `'use client'` at the top of any file that renders it in a Next.js App Router project.

## Accessibility

- The price legend region uses `role="region"` with `aria-labelledby` pointing to its title element, so screen readers announce the landmark when entering it.
- Legend items are grouped in `role="list"` with `role="listitem"` on each entry.
- Navigation button labels support month-name token interpolation, producing a fully spoken sentence (e.g. _"See August 2025 and September 2025"_).
- When `showPriceLegend` is enabled, `statusLabels` are derived from the legend text and injected as visually-hidden `<span class="sr-only">` elements inside annotated day buttons, so each day's status is announced by screen readers.
- The reinsurance disclaimer is a plain `<p>` element; no additional ARIA attributes are required.
