CriteriaParticipants

PreviousNext

Adult and child participant selector with three children-flow modes, per-child birthdate or age/month entry, animated transitions, participant chip summary, and an optional limit banner.

Participant

Enter date of birth

2 adults, 0 children

'use client';
import { useState } from 'react';
import { subYears } from 'date-fns';

import { CriteriaParticipants } from '@/ui/criteria/CriteriaParticipants';
import type { CriteriaParticipantsAction } from '@/ui/criteria/CriteriaParticipants';

const labels = {
  addAdult: 'Add adult',
  addChild: 'Add child',
  adults: 'Adults',
  children: 'Children',
  childrenAgeLimit: 'Under 18 y.o.',
  childBirthdate: 'Child {n} birthdate',
  childBirthdateInvalidDateError: 'Invalid date',
  childBirthdateMaxAgeError: 'Too young (under 2)',
  childBirthdateMinAgeError: 'Too old (over 17)',
  removeAdult: 'Remove adult',
  removeChild: 'Remove child',
  enterDateOfBirth: 'Enter date of birth',
  ageQuestion: 'How old will the children be at check-in?',
  ageYear: 'Year',
  ageMonths: 'Months',
  ageApply: 'Apply',
  participantDetails: 'Participant Details',
  adultLabel: 'Adult {n}',
  childLabel: 'Child {n}',
  removeParticipant: 'Remove',
  reset: 'Reset',
  apply: 'Continue',
};

export function CriteriaParticipantsDemo() {
  const [adultsCount, setAdults] = useState(2);
  const [childrenCount, setChildren] = useState(0);
  const [birthdates, setBirthdates] = useState<Array<Date | null | undefined>>([]);
  const [childAges, setChildAges] = useState<
    Array<{ value: number; unit: 'year' | 'month' } | null>
  >([]);

  const handleChange = (action: CriteriaParticipantsAction) => {
    if (action.type === 'adultsCount') setAdults(action.payload);
    if (action.type === 'childrenCount') {
      setChildren(action.payload);
      setBirthdates((prev) =>
        action.payload > prev.length ? [...prev, null] : prev.slice(0, action.payload),
      );
      setChildAges((prev) =>
        action.payload > prev.length
          ? [...prev, ...Array(action.payload - prev.length).fill(null)]
          : prev.slice(0, action.payload),
      );
    }
    if (action.type === 'birthdate') {
      setBirthdates((prev) => {
        const next = [...prev];
        next[action.payload.index] = action.payload.date;
        return next;
      });
    }
    if (action.type === 'childAge') {
      setChildAges((prev) => {
        const next = [...prev];
        next[action.payload.index] =
          action.payload.value !== null
            ? { value: action.payload.value, unit: action.payload.unit }
            : null;
        return next;
      });
    }
  };

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaParticipants
        labels={labels}
        adultsCount={adultsCount}
        childrenCount={childrenCount}
        birthdates={birthdates}
        childAges={childAges}
        maxAuthorizedBirthdate={subYears(new Date(), 2)}
        minAuthorizedBirthdate={subYears(new Date(), 17)}
        onChange={handleChange}
        title="Participant"
        onReset={() => {
          setAdults(2);
          setChildren(0);
          setBirthdates([]);
          setChildAges([]);
        }}
        onApply={() => {}}
      />
      <p className="text-b4">
        {adultsCount} adult{adultsCount !== 1 ? 's' : ''}, {childrenCount} child
        {childrenCount !== 1 ? 'ren' : ''}
      </p>
    </div>
  );
}

Installation

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

Dependencies

Install the date-fns peer dependency used for birthdate bound calculations:

npm install date-fns

Usage

Basic controlled usage

CriteriaParticipants is fully controlled. The parent manages adultsCount, childrenCount, the birthdates array, and optionally childAges, and updates them through a single onChange dispatcher that receives a typed action object.

Participant

Enter date of birth

2 adults, 0 children

'use client';
import { useState } from 'react';
import { subYears } from 'date-fns';

import { CriteriaParticipants } from '@/ui/criteria/CriteriaParticipants';
import type { CriteriaParticipantsAction } from '@/ui/criteria/CriteriaParticipants';

const labels = {
  addAdult: 'Add adult',
  addChild: 'Add child',
  adults: 'Adults',
  children: 'Children',
  childrenAgeLimit: 'Under 18 y.o.',
  childBirthdate: 'Child {n} birthdate',
  childBirthdateInvalidDateError: 'Invalid date',
  childBirthdateMaxAgeError: 'Too young (under 2)',
  childBirthdateMinAgeError: 'Too old (over 17)',
  removeAdult: 'Remove adult',
  removeChild: 'Remove child',
  enterDateOfBirth: 'Enter date of birth',
  ageQuestion: 'How old will the children be at check-in?',
  ageYear: 'Year',
  ageMonths: 'Months',
  ageApply: 'Apply',
  participantDetails: 'Participant Details',
  adultLabel: 'Adult {n}',
  childLabel: 'Child {n}',
  removeParticipant: 'Remove',
  reset: 'Reset',
  apply: 'Continue',
};

export function CriteriaParticipantsDemo() {
  const [adultsCount, setAdults] = useState(2);
  const [childrenCount, setChildren] = useState(0);
  const [birthdates, setBirthdates] = useState<Array<Date | null | undefined>>([]);
  const [childAges, setChildAges] = useState<
    Array<{ value: number; unit: 'year' | 'month' } | null>
  >([]);

  const handleChange = (action: CriteriaParticipantsAction) => {
    if (action.type === 'adultsCount') setAdults(action.payload);
    if (action.type === 'childrenCount') {
      setChildren(action.payload);
      setBirthdates((prev) =>
        action.payload > prev.length ? [...prev, null] : prev.slice(0, action.payload),
      );
      setChildAges((prev) =>
        action.payload > prev.length
          ? [...prev, ...Array(action.payload - prev.length).fill(null)]
          : prev.slice(0, action.payload),
      );
    }
    if (action.type === 'birthdate') {
      setBirthdates((prev) => {
        const next = [...prev];
        next[action.payload.index] = action.payload.date;
        return next;
      });
    }
    if (action.type === 'childAge') {
      setChildAges((prev) => {
        const next = [...prev];
        next[action.payload.index] =
          action.payload.value !== null
            ? { value: action.payload.value, unit: action.payload.unit }
            : null;
        return next;
      });
    }
  };

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaParticipants
        labels={labels}
        adultsCount={adultsCount}
        childrenCount={childrenCount}
        birthdates={birthdates}
        childAges={childAges}
        maxAuthorizedBirthdate={subYears(new Date(), 2)}
        minAuthorizedBirthdate={subYears(new Date(), 17)}
        onChange={handleChange}
        title="Participant"
        onReset={() => {
          setAdults(2);
          setChildren(0);
          setBirthdates([]);
          setChildAges([]);
        }}
        onApply={() => {}}
      />
      <p className="text-b4">
        {adultsCount} adult{adultsCount !== 1 ? 's' : ''}, {childrenCount} child
        {childrenCount !== 1 ? 'ren' : ''}
      </p>
    </div>
  );
}

Children flows

childrenFlow controls how children enter their age information, whether an Apply button appears, and whether participant chips are shown. There are three modes.

Flow 1 — Birthdate only

The simplest mode. Only birthdate fields are shown — no toggle, no year/month inputs. onChange fires immediately on each birthdate change. No Apply button, no participant chips.

Participants

Birthdates: none

'use client';
import { useState } from 'react';
import { format, subYears } from 'date-fns';

import { CriteriaParticipants } from '@/ui/criteria/CriteriaParticipants';
import type { CriteriaParticipantsAction } from '@/ui/criteria/CriteriaParticipants';

const labels = {
  addAdult: 'Add adult',
  addChild: 'Add child',
  adults: 'Adults',
  children: 'Children',
  childrenAgeLimit: 'Under 18 y.o.',
  childBirthdate: 'Child {n} birthdate',
  childBirthdateInvalidDateError: 'Invalid date',
  childBirthdateMaxAgeError: 'Too young (under 2)',
  childBirthdateMinAgeError: 'Too old (over 17)',
  removeAdult: 'Remove adult',
  removeChild: 'Remove child',
};

export function CriteriaParticipantsFlow1Demo() {
  const [adultsCount, setAdults] = useState(2);
  const [childrenCount, setChildren] = useState(0);
  const [birthdates, setBirthdates] = useState<Array<Date | null | undefined>>([]);

  const handleChange = (action: CriteriaParticipantsAction) => {
    if (action.type === 'adultsCount') setAdults(action.payload);
    if (action.type === 'childrenCount') {
      setChildren(action.payload);
      setBirthdates((prev) =>
        action.payload > prev.length ? [...prev, null] : prev.slice(0, action.payload),
      );
    }
    if (action.type === 'birthdate') {
      setBirthdates((prev) => {
        const next = [...prev];
        next[action.payload.index] = action.payload.date;
        return next;
      });
    }
  };

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaParticipants
        childrenFlow="birthdate-only"
        labels={labels}
        adultsCount={adultsCount}
        childrenCount={childrenCount}
        birthdates={birthdates}
        maxAuthorizedBirthdate={subYears(new Date(), 2)}
        minAuthorizedBirthdate={subYears(new Date(), 17)}
        onChange={handleChange}
        title="Participants"
      />
      <p className="text-b4">
        Birthdates:{' '}
        {birthdates.map((d) => (d ? format(d, 'yyyy-MM-dd') : '—')).join(', ') || 'none'}
      </p>
    </div>
  );
}

Flow 2 — Age or birthdate (immediate)

The user toggles between a birthdate field and year/month age inputs per child. onChange fires immediately — the per-child Apply button commits each age entry as soon as it is clicked. No footer Apply button, no participant chips.

Participants

Enter date of birth

No ages or birthdates entered yet

'use client';
import { useState } from 'react';
import { format, subYears } from 'date-fns';

import { CriteriaParticipants } from '@/ui/criteria/CriteriaParticipants';
import type { CriteriaParticipantsAction } from '@/ui/criteria/CriteriaParticipants';

const labels = {
  addAdult: 'Add adult',
  addChild: 'Add child',
  adults: 'Adults',
  children: 'Children',
  childrenAgeLimit: 'Under 18 y.o.',
  childBirthdate: 'Child {n} birthdate',
  childBirthdateInvalidDateError: 'Invalid date',
  childBirthdateMaxAgeError: 'Too young (under 2)',
  childBirthdateMinAgeError: 'Too old (over 17)',
  removeAdult: 'Remove adult',
  removeChild: 'Remove child',
  enterDateOfBirth: 'Enter date of birth',
  ageQuestion: 'How old will the children be at check-in?',
  ageYear: 'Year',
  ageMonths: 'Months',
  ageApply: 'Apply',
};

export function CriteriaParticipantsFlow2Demo() {
  const [adultsCount, setAdults] = useState(2);
  const [childrenCount, setChildren] = useState(0);
  const [birthdates, setBirthdates] = useState<Array<Date | null | undefined>>([]);
  const [childAges, setChildAges] = useState<
    Array<{ value: number; unit: 'year' | 'month' } | null>
  >([]);

  const handleChange = (action: CriteriaParticipantsAction) => {
    if (action.type === 'adultsCount') setAdults(action.payload);
    if (action.type === 'childrenCount') {
      setChildren(action.payload);
      setBirthdates((prev) =>
        action.payload > prev.length ? [...prev, null] : prev.slice(0, action.payload),
      );
      setChildAges((prev) =>
        action.payload > prev.length
          ? [...prev, ...Array(action.payload - prev.length).fill(null)]
          : prev.slice(0, action.payload),
      );
    }
    if (action.type === 'birthdate') {
      setBirthdates((prev) => {
        const next = [...prev];
        next[action.payload.index] = action.payload.date;
        return next;
      });
    }
    if (action.type === 'childAge') {
      setChildAges((prev) => {
        const next = [...prev];
        next[action.payload.index] =
          action.payload.value !== null
            ? { value: action.payload.value, unit: action.payload.unit }
            : null;
        return next;
      });
    }
  };

  const summary = [
    ...birthdates.map((d, i) => (d ? `Child ${i + 1}: ${format(d, 'yyyy-MM-dd')}` : null)),
    ...childAges.map((a, i) =>
      a ? `Child ${i + 1}: ${a.value} ${a.unit === 'year' ? 'yr' : 'mo'}` : null,
    ),
  ]
    .filter(Boolean)
    .join(', ');

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaParticipants
        childrenFlow="age-or-birthdate"
        labels={labels}
        adultsCount={adultsCount}
        childrenCount={childrenCount}
        birthdates={birthdates}
        childAges={childAges}
        maxAuthorizedBirthdate={subYears(new Date(), 2)}
        minAuthorizedBirthdate={subYears(new Date(), 17)}
        onChange={handleChange}
        title="Participants"
      />
      <p className="text-b4">{summary || 'No ages or birthdates entered yet'}</p>
    </div>
  );
}

Flow 3 — Age or birthdate with Apply (default)

The same toggle as flow 2. A footer Apply button lets the user confirm their full selection. Participant chips appear showing each confirmed adult and child with their age. Clicking a chip removes that participant. Pass onReset and onApply to wire up the footer actions.

Participant

Enter date of birth

Not yet confirmed

'use client';
import { useState } from 'react';
import { subYears } from 'date-fns';

import { CriteriaParticipants } from '@/ui/criteria/CriteriaParticipants';
import type { CriteriaParticipantsAction } from '@/ui/criteria/CriteriaParticipants';

const labels = {
  addAdult: 'Add adult',
  addChild: 'Add child',
  adults: 'Adults',
  children: 'Children',
  childrenAgeLimit: 'Under 18 y.o.',
  childBirthdate: 'Child {n} birthdate',
  childBirthdateInvalidDateError: 'Invalid date',
  childBirthdateMaxAgeError: 'Too young (under 2)',
  childBirthdateMinAgeError: 'Too old (over 17)',
  removeAdult: 'Remove adult',
  removeChild: 'Remove child',
  enterDateOfBirth: 'Enter date of birth',
  ageQuestion: 'How old will the children be at check-in?',
  ageYear: 'Year',
  ageMonths: 'Months',
  ageApply: 'Apply',
  participantDetails: 'Participant Details',
  adultLabel: 'Adult {n}',
  childLabel: 'Child {n}',
  removeParticipant: 'Remove',
  reset: 'Reset',
  apply: 'Continue',
};

export function CriteriaParticipantsFlow3Demo() {
  const [adultsCount, setAdults] = useState(2);
  const [childrenCount, setChildren] = useState(0);
  const [birthdates, setBirthdates] = useState<Array<Date | null | undefined>>([]);
  const [childAges, setChildAges] = useState<
    Array<{ value: number; unit: 'year' | 'month' } | null>
  >([]);
  const [confirmed, setConfirmed] = useState('');

  const handleChange = (action: CriteriaParticipantsAction) => {
    if (action.type === 'adultsCount') setAdults(action.payload);
    if (action.type === 'childrenCount') {
      setChildren(action.payload);
      setBirthdates((prev) =>
        action.payload > prev.length ? [...prev, null] : prev.slice(0, action.payload),
      );
      setChildAges((prev) =>
        action.payload > prev.length
          ? [...prev, ...Array(action.payload - prev.length).fill(null)]
          : prev.slice(0, action.payload),
      );
    }
    if (action.type === 'birthdate') {
      setBirthdates((prev) => {
        const next = [...prev];
        next[action.payload.index] = action.payload.date;
        return next;
      });
    }
    if (action.type === 'childAge') {
      setChildAges((prev) => {
        const next = [...prev];
        next[action.payload.index] =
          action.payload.value !== null
            ? { value: action.payload.value, unit: action.payload.unit }
            : null;
        return next;
      });
    }
  };

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaParticipants
        childrenFlow="age-or-birthdate-with-apply"
        labels={labels}
        adultsCount={adultsCount}
        childrenCount={childrenCount}
        birthdates={birthdates}
        childAges={childAges}
        maxAuthorizedBirthdate={subYears(new Date(), 2)}
        minAuthorizedBirthdate={subYears(new Date(), 17)}
        onChange={handleChange}
        title="Participant"
        onReset={() => {
          setAdults(2);
          setChildren(0);
          setBirthdates([]);
          setChildAges([]);
          setConfirmed('');
        }}
        onApply={() => {
          setConfirmed(
            `${adultsCount} adult${adultsCount !== 1 ? 's' : ''}, ${childrenCount} child${childrenCount !== 1 ? 'ren' : ''}`,
          );
        }}
      />
      <p className="text-b4">{confirmed ? `Confirmed: ${confirmed}` : 'Not yet confirmed'}</p>
    </div>
  );
}

Flow comparison

childrenFlowAge toggleFooter ApplyParticipant chipsonChange fires on
birthdate-onlyNoNoNoEach birthdate change
age-or-birthdateYesNoNoEach birthdate change or per-child Apply click
age-or-birthdate-with-apply (default)YesYesYesEach birthdate change or per-child Apply click

Variants

Card (default)

Sand background (bg-lightSand, rounded-16, p-20) wrapping white rounded containers for each field group. Use this in floating search panels, resort selectors, and booking drawers.

Flat

No background or card chrome. Fields are rendered directly with space-y-20 spacing. Birthdate inputs are laid out in a single column on mobile and a two-column grid on medium screens. Use inside a form layout that provides its own container.

<CriteriaParticipants
  variant="flat"
  labels={labels}
  adultsCount={adultsCount}
  childrenCount={childrenCount}
  birthdates={birthdates}
  maxAuthorizedBirthdate={subYears(new Date(), 2)}
  onChange={handleChange}
/>

More usage

Without children

Set withChildren={false} to hide the children section entirely. This is useful for booking flows where children are not permitted, or when they are handled in a separate step.

<CriteriaParticipants
  withChildren={false}
  labels={labels}
  adultsCount={adultsCount}
  childrenCount={0}
  birthdates={[]}
  maxAuthorizedBirthdate={subYears(new Date(), 2)}
  onChange={handleChange}
/>

With participant limit banner

When the combined total of adults and children reaches maxTotalParticipants (default 6), a limit banner appears. Pass the full limit object inside labels to enable it. The optional onLimitDisplay callback fires exactly once the first time the total is reached.

const labels = {
  // ...other labels
  limit: {
    title: 'Maximum participants reached',
    description: 'Our resorts accommodate a maximum of 6 guests per booking.',
    contacts: [
      {
        title: 'Call us',
        description: 'Mon–Fri, 9 am – 6 pm',
        href: 'tel:+33123456789',
        icon: 'Phone',
        target: '_self',
      },
    ],
    labels: { expand: 'Read more', reduce: 'Read less' },
  },
};
 
<CriteriaParticipants
  labels={labels}
  maxTotalParticipants={6}
  onLimitDisplay={() => analytics.track('participant_limit_reached')}
  adultsCount={adultsCount}
  childrenCount={childrenCount}
  birthdates={birthdates}
  maxAuthorizedBirthdate={subYears(new Date(), 2)}
  onChange={handleChange}
/>

Adults-only description

Pass adultsDescription inside labels to show a sub-label below the adults field label. The hidden input aria-label automatically combines the label and description for screen readers.

<CriteriaParticipants
  labels={{ ...labels, adultsDescription: '18 years and over' }}
  adultsCount={adultsCount}
  childrenCount={childrenCount}
  birthdates={birthdates}
  maxAuthorizedBirthdate={subYears(new Date(), 2)}
  onChange={handleChange}
/>

API Reference

CriteriaParticipants

PropTypeDefaultDescription
labelsCriteriaParticipantsLabels—Required. All user-facing strings. See CriteriaParticipantsLabels.
adultsCountnumber—Required. Controlled adults count.
childrenCountnumber—Required. Controlled children count.
birthdatesArray<Date | null | undefined>—Required. One entry per child. Length must match childrenCount.
maxAuthorizedBirthdateDate—Required. Latest valid birthdate (child is too young if born after this). Typically subYears(new Date(), minAge).
onChangeDispatch<CriteriaParticipantsAction>—Required. Dispatcher for all state changes. See CriteriaParticipantsAction.
childrenFlow'birthdate-only' | 'age-or-birthdate' | 'age-or-birthdate-with-apply''age-or-birthdate-with-apply'Controls the children age-entry UX, Apply button visibility, and chip display.
childAgesArray<{ value: number; unit: 'year' | 'month' } | null | undefined>undefinedApplied child ages for year/month mode. One entry per child.
minAdultsnumberundefinedMinimum adults count. Decrease button is disabled at this value.
maxAdultsnumberundefinedMaximum adults count. Increase button is disabled at this value.
minChildrennumberundefinedMinimum children count.
maxChildrennumberundefinedMaximum children count.
withChildrenbooleantrueWhen false, hides the children section entirely.
minAuthorizedBirthdateDatetodayEarliest valid birthdate (child is too old if born before this). Typically subYears(new Date(), maxAge).
maxTotalParticipantsnumber6Combined adults + children at which the limit banner appears.
variant'card' | 'flat''card'Visual layout variant.
titlestringundefinedHeading at the top of the component.
classNamestringundefinedAdditional CSS class on the root element.
onLimitDisplay() => voidundefinedFired once when the total first reaches maxTotalParticipants.
onReset() => voidundefinedFooter Reset button handler. Button only renders when this is provided and labels.reset is set.
onApply() => voidundefinedFooter Apply button handler. Button only renders in age-or-birthdate-with-apply flow when this and labels.apply are set.

CriteriaParticipantsLabels

FieldTypeDescription
adultsstringAdults field label.
adultsDescriptionstring | null | undefinedOptional sub-label under the adults field (e.g. "18 years and over").
addAdultstringaria-label for the adults increment button.
removeAdultstringaria-label for the adults decrement button.
childrenstringChildren field label.
childrenAgeLimitstringAge-range hint shown under the children field label (e.g. "2–17 years").
addChildstringaria-label for the children increment button.
removeChildstringaria-label for the children decrement button.
childBirthdatestringBirthdate field label. Use {n} for the 1-based child index.
childBirthdateInvalidDateErrorstringError when the entered value is not a valid date.
childBirthdateMinAgeErrorstringError when the child exceeds the maximum allowed age.
childBirthdateMaxAgeErrorstringError when the child is below the minimum allowed age.
enterDateOfBirthstring | undefinedToggle label for switching between birthdate and age inputs. Omit to suppress the toggle (birthdate-only always shows birthdate fields; other flows always show age inputs).
ageQuestionstring | undefinedQuestion text shown above the per-child age inputs.
ageYearstring | undefined"Year" tab label in the year/month Toggle.
ageMonthsstring | undefined"Months" tab label in the year/month Toggle.
ageApplystring | undefinedPer-child Apply button label.
participantDetailsstring | undefinedCollapsible chip section title. Omit to hide the chip section entirely.
adultLabelstring | undefinedChip label template for adults. Use {n} for the 1-based index (e.g. "Adult {n}").
childLabelstring | undefinedChip label template for children. Use {n} for the 1-based index (e.g. "Child {n}").
removeParticipantstring | undefinedaria-label for chip remove buttons.
resetstring | undefinedFooter Reset link label.
applystring | undefinedFooter Apply button label.
limitParticipantsLimit | undefinedContent for the limit banner. Omit to suppress the banner entirely.

CriteriaParticipantsAction

The onChange dispatcher receives one of four action shapes:

type CriteriaParticipantsAction =
  | { type: 'adultsCount'; payload: number }
  | { type: 'childrenCount'; payload: number }
  | { type: 'birthdate'; payload: { date: Date | null; index: number } }
  | { type: 'childAge'; payload: { index: number; value: number | null; unit: 'year' | 'month' } };
  • adultsCount — dispatched by the adults NumberField. payload is the new count.
  • childrenCount — dispatched by the children NumberField. payload is the new count. Resize the birthdates and childAges arrays to match in the same handler.
  • birthdate — dispatched when a child's birthdate field changes. payload.index is the 0-based child position; payload.date is the new Date or null when cleared.
  • childAge — dispatched when a per-child Apply button is clicked in age mode. payload.index is the 0-based child position; payload.value is the age (null if input was empty); payload.unit is 'year' or 'month'.

ParticipantsLimit

Provided via labels.limit. Omit the key to disable the limit banner entirely.

FieldTypeDescription
titlestringBanner heading. Displayed next to an error icon.
descriptionstringBody text shown in the expandable section.
contactsParticipantsLimitContact[]Zero or more contact links rendered below the description.
labels.expandstring"Read more" toggle label.
labels.reducestring"Read less" toggle label.

ParticipantsLimitContact

FieldTypeDescription
titlestringLink heading.
descriptionstring | nullOptional sub-description below the title.
hrefstringLink href (e.g. tel:+33...).
iconstringIcon name rendered in the link chip.
targetstringLink target attribute (e.g. '_self', '_blank').

Behaviour

Keeping arrays in sync

The birthdates and childAges array lengths must match childrenCount at all times. The recommended pattern is to grow or shrink both arrays inside the childrenCount branch of onChange: append null for each new child, and truncate when children are removed.

Limit banner

The limit banner appears once the first time adultsCount + childrenCount >= maxTotalParticipants. It remains visible for the rest of the session. The onLimitDisplay callback fires exactly once per mount at the moment the threshold is first crossed.

Birthdate validation

Each birthdate field validates four conditions in priority order:

  1. Invalid date — the entered value is not a parseable calendar date.
  2. Future date — the entered birthdate is after today.
  3. Too old — birthdate is before minAuthorizedBirthdate.
  4. Too young — birthdate is after maxAuthorizedBirthdate.

minDateErrorMessage is shown for condition 3 and maxDateErrorMessage for condition 4 — named from the perspective of the date bound, not the age direction.

Participant chips

The chip section is collapsible and only appears in age-or-birthdate-with-apply flow when labels.participantDetails is provided. One chip is shown per confirmed adult and child. Child chips display the applied age or computed birthdate age as a label. Clicking any chip decrements the corresponding count by one.

childrenFlow and the Apply button

The footer Apply button only renders in age-or-birthdate-with-apply flow. In flows 1 and 2, onApply is accepted but the button is suppressed. The onReset footer link is shown in all flows when provided.

Accessibility

  • Adults and children NumberField components expose labelled increment and decrement buttons. The hidden <input> carries an aria-label combining the field label with any description.
  • Birthdate fields render with a visible label and an inline error message associated via aria-describedby.
  • The year/month Toggle uses aria-pressed on each button and role="group" on the container.
  • The limit banner heading is assigned a unique id used by the expandable section for aria-controls association.
  • Participant chips are <button> elements with an aria-label from labels.removeParticipant.
  • The chip section header is a <button> with aria-expanded reflecting its open/closed state.
  • The CrossDissolve transition between birthdate and age fields marks the outgoing slot as aria-hidden during the animation to prevent duplicate announcements.