- Accordion
- Advanced Toast
- AlertMessage
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- Chapters
- ChatBody
- ChatButton
- ChatHeader
- ChatInput
- ChatMessageActions
- ChatMessageEditor
- ChatMessage
- ChatTypingIndicator
- ChatWindowCsatPrompt
- ChatWindowQuote
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- ContentReview
- Countdown
- CriteriaDates
- CriteriaParticipants
- CriteriaTransports
- Date Field
- Datepicker
- Dropdown
- ElasticHeight
- ExpandableCard
- ExpandableText
- Filter
- FloatingLabelTextField
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- JourneyCard
- LayerHeader
- Layer
- Link
- Loader
- Number Field
- PageNavigation
- Pagination
- Paragraph
- Password
- Phone Field
- Popin
- Portal
- ProductThumbnail
- Promises
- Prose
- Radio Group
- RadioSelect
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- SparkleWrapper
- Spinner
- Switch
- Table
- Tabs
- Tag
- TextField
- Toggle
- Tooltip
- ValidationMessage
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
childrenFlow | Age toggle | Footer Apply | Participant chips | onChange fires on |
|---|---|---|---|---|
birthdate-only | No | No | No | Each birthdate change |
age-or-birthdate | Yes | No | No | Each birthdate change or per-child Apply click |
age-or-birthdate-with-apply (default) | Yes | Yes | Yes | Each 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
| Prop | Type | Default | Description |
|---|---|---|---|
labels | CriteriaParticipantsLabels | — | Required. All user-facing strings. See CriteriaParticipantsLabels. |
adultsCount | number | — | Required. Controlled adults count. |
childrenCount | number | — | Required. Controlled children count. |
birthdates | Array<Date | null | undefined> | — | Required. One entry per child. Length must match childrenCount. |
maxAuthorizedBirthdate | Date | — | Required. Latest valid birthdate (child is too young if born after this). Typically subYears(new Date(), minAge). |
onChange | Dispatch<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. |
childAges | Array<{ value: number; unit: 'year' | 'month' } | null | undefined> | undefined | Applied child ages for year/month mode. One entry per child. |
minAdults | number | undefined | Minimum adults count. Decrease button is disabled at this value. |
maxAdults | number | undefined | Maximum adults count. Increase button is disabled at this value. |
minChildren | number | undefined | Minimum children count. |
maxChildren | number | undefined | Maximum children count. |
withChildren | boolean | true | When false, hides the children section entirely. |
minAuthorizedBirthdate | Date | today | Earliest valid birthdate (child is too old if born before this). Typically subYears(new Date(), maxAge). |
maxTotalParticipants | number | 6 | Combined adults + children at which the limit banner appears. |
variant | 'card' | 'flat' | 'card' | Visual layout variant. |
title | string | undefined | Heading at the top of the component. |
className | string | undefined | Additional CSS class on the root element. |
onLimitDisplay | () => void | undefined | Fired once when the total first reaches maxTotalParticipants. |
onReset | () => void | undefined | Footer Reset button handler. Button only renders when this is provided and labels.reset is set. |
onApply | () => void | undefined | Footer Apply button handler. Button only renders in age-or-birthdate-with-apply flow when this and labels.apply are set. |
CriteriaParticipantsLabels
| Field | Type | Description |
|---|---|---|
adults | string | Adults field label. |
adultsDescription | string | null | undefined | Optional sub-label under the adults field (e.g. "18 years and over"). |
addAdult | string | aria-label for the adults increment button. |
removeAdult | string | aria-label for the adults decrement button. |
children | string | Children field label. |
childrenAgeLimit | string | Age-range hint shown under the children field label (e.g. "2–17 years"). |
addChild | string | aria-label for the children increment button. |
removeChild | string | aria-label for the children decrement button. |
childBirthdate | string | Birthdate field label. Use {n} for the 1-based child index. |
childBirthdateInvalidDateError | string | Error when the entered value is not a valid date. |
childBirthdateMinAgeError | string | Error when the child exceeds the maximum allowed age. |
childBirthdateMaxAgeError | string | Error when the child is below the minimum allowed age. |
enterDateOfBirth | string | undefined | Toggle 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). |
ageQuestion | string | undefined | Question text shown above the per-child age inputs. |
ageYear | string | undefined | "Year" tab label in the year/month Toggle. |
ageMonths | string | undefined | "Months" tab label in the year/month Toggle. |
ageApply | string | undefined | Per-child Apply button label. |
participantDetails | string | undefined | Collapsible chip section title. Omit to hide the chip section entirely. |
adultLabel | string | undefined | Chip label template for adults. Use {n} for the 1-based index (e.g. "Adult {n}"). |
childLabel | string | undefined | Chip label template for children. Use {n} for the 1-based index (e.g. "Child {n}"). |
removeParticipant | string | undefined | aria-label for chip remove buttons. |
reset | string | undefined | Footer Reset link label. |
apply | string | undefined | Footer Apply button label. |
limit | ParticipantsLimit | undefined | Content 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 adultsNumberField.payloadis the new count.childrenCount— dispatched by the childrenNumberField.payloadis the new count. Resize thebirthdatesandchildAgesarrays to match in the same handler.birthdate— dispatched when a child's birthdate field changes.payload.indexis the 0-based child position;payload.dateis the newDateornullwhen cleared.childAge— dispatched when a per-child Apply button is clicked in age mode.payload.indexis the 0-based child position;payload.valueis the age (nullif input was empty);payload.unitis'year'or'month'.
ParticipantsLimit
Provided via labels.limit. Omit the key to disable the limit banner entirely.
| Field | Type | Description |
|---|---|---|
title | string | Banner heading. Displayed next to an error icon. |
description | string | Body text shown in the expandable section. |
contacts | ParticipantsLimitContact[] | Zero or more contact links rendered below the description. |
labels.expand | string | "Read more" toggle label. |
labels.reduce | string | "Read less" toggle label. |
ParticipantsLimitContact
| Field | Type | Description |
|---|---|---|
title | string | Link heading. |
description | string | null | Optional sub-description below the title. |
href | string | Link href (e.g. tel:+33...). |
icon | string | Icon name rendered in the link chip. |
target | string | Link 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:
- Invalid date — the entered value is not a parseable calendar date.
- Future date — the entered birthdate is after today.
- Too old — birthdate is before
minAuthorizedBirthdate. - 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
NumberFieldcomponents expose labelled increment and decrement buttons. The hidden<input>carries anaria-labelcombining 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
Toggleusesaria-pressedon each button androle="group"on the container. - The limit banner heading is assigned a unique
idused by the expandable section foraria-controlsassociation. - Participant chips are
<button>elements with anaria-labelfromlabels.removeParticipant. - The chip section header is a
<button>witharia-expandedreflecting its open/closed state. - The
CrossDissolvetransition between birthdate and age fields marks the outgoing slot asaria-hiddenduring the animation to prevent duplicate announcements.
On This Page
InstallationDependenciesUsageBasic controlled usageChildren flowsFlow 1 — Birthdate onlyFlow 2 — Age or birthdate (immediate)Flow 3 — Age or birthdate with Apply (default)Flow comparisonVariantsCard (default)FlatMore usageWithout childrenWith participant limit bannerAdults-only descriptionAPI ReferenceCriteriaParticipantsCriteriaParticipantsLabelsCriteriaParticipantsActionParticipantsLimitParticipantsLimitContactBehaviourKeeping arrays in syncLimit bannerBirthdate validationParticipant chipschildrenFlow and the Apply buttonAccessibility