Components
Counter Input
BetaIncrement and decrement buttons around a number display. Used for adult count, child count, and room count — a simpler standalone alternative to TravellerSelector for compact contexts like filter panels.
Purpose
The CounterInput gives users precise numeric control through dedicated − and + buttons, avoiding the imprecision of a free-text field for small bounded ranges. It is used wherever a user must select a count from a known minimum/maximum:
- Traveller counts — adults and children on a search form or booking flow
- Room count — number of rooms required
- Item quantities — bags, tours, optional extras
- Filter panels — "minimum star rating", "minimum nights"
The CounterInput enforces its min and max by disabling the corresponding button at the boundary — users cannot enter an invalid value.
Usage
Reach for CounterInput when the value is a small bounded count controlled entirely via + and − buttons — traveller counts, room counts, quantities. For free-form numeric entry without hard bounds (a budget figure, a year), use Input with type="number" instead. For selecting a value from a continuous range (a price band, a star-rating minimum), use RangeSlider.
CounterInput appears in compact contexts — filter panels, quantity steppers in a booking summary — while the more detailed TravellerSelector handles the primary search widget where children's ages also need to be captured.
Anatomy
- 1Label — Text above the counter identifying what is being counted. 14 px medium weight.
- 2Decrement button — 36 px square ghost-style button with Minus icon. Disabled at min value.
- 3Value display — Current numeric value centred between the buttons. Semibold, tabular numerals.
- 4Increment button — 36 px square ghost-style button with Plus icon. Disabled at max value.
Variants
Default
At minimum (decrement disabled)
At maximum (increment disabled)
Disabled
All controls are non-interactive.
Best practices
Set min to the lowest valid value for the context. Adults min=1; children min=0.
Adults min=1 prevents submitting a search with zero adults — a guaranteed invalid state.
Set min=0 for fields where 0 is never a valid answer.
A counter with min=0 for adults allows a search with 0 travellers — a meaningless query.
Always provide a label that names what is being counted.
Distinct label='Adults', label='Children', label='Rooms' lets users understand each counter without reading surrounding context.
Render CounterInput without a label — the value alone is ambiguous.
Three unlabelled counters in a row give no indication of which count belongs to which traveller type.
Set max to a practical upper limit that reflects system constraints.
A max of 9 adults reflects a reasonable group booking limit and prevents obviously invalid values.
Leave max at its default (99) for values with real-world booking constraints.
Unlimited counters allow users to request 50 rooms — a state the booking system cannot fulfil.
Use CounterInput in filter panels and compact forms; use TravellerSelector in the primary search widget.
The ComprehensiveTravellerSelector with dropdowns handles children's ages — CounterInput is for compact contexts.
Use CounterInput for children counts in a booking flow without capturing their ages elsewhere.
Children's ages cannot be entered in a CounterInput — the booking system needs them for pricing.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | number | 0 | Initial value for uncontrolled usage. |
| value | number | — | Controlled value. When provided, parent must handle onChange to update it. |
| min | number | 0 | Minimum allowed value. Decrement button is disabled when value equals min. |
| max | number | 99 | Maximum allowed value. Increment button is disabled when value equals max. |
| step | number | 1 | Amount to increment or decrement on each button press. |
| onChange | (value: number) => void | — | Called with the new clamped value after each button press. |
| disabled | boolean | false | Disables both buttons. |
| label | string | — | Visible label above the counter. Required for accessibility when used without surrounding context. |
| id | string | auto-generated | Passed to the label ID. Auto-generated with useId when omitted. |
| className | string | — | Additional Tailwind classes on the root container. |
Accessibility
Live region
The current value is rendered inside an <output> element with aria-live="polite" and aria-atomic="true":
<output aria-live="polite" aria-atomic="true">
{current}
</output>
Each time the user presses + or −, the new value is announced by screen readers without interrupting other speech.
Button labels
Each button has an aria-label that includes the label prop:
<button aria-label="Decrease Adults">
<Minus />
</button>
<button aria-label="Increase Adults">
<Plus />
</button>
This ensures screen readers announce "Decrease Adults" and "Increase Adults" — not just "button".
Group labelling
When label is provided, the counter group has aria-labelledby pointing to the label element, grouping the decrement, value, and increment under the same accessible name.
Keyboard interaction
| Key | Action |
|---|---|
Tab | Move to decrement button |
Tab again | Move to increment button |
Enter / Space | Activate the focused button |
Related components
- Input — use for free-form numeric entry without stepped +/− controls.
- RangeSlider — use for selecting a value from a continuous range rather than a discrete count.