KKUONIDesign System

Components

Counter Input

Beta

Increment 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.

Adults
2
Children
0
Rooms
1

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

Adults
2
  1. 1
    LabelText above the counter identifying what is being counted. 14 px medium weight.
  2. 2
    Decrement button36 px square ghost-style button with Minus icon. Disabled at min value.
  3. 3
    Value displayCurrent numeric value centred between the buttons. Semibold, tabular numerals.
  4. 4
    Increment button36 px square ghost-style button with Plus icon. Disabled at max value.

Variants

Default

Adults
2

At minimum (decrement disabled)

Children
0

At maximum (increment disabled)

Rooms
3

Disabled

All controls are non-interactive.

Infants
0

Best practices

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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

PropTypeDefaultDescription
defaultValuenumber0Initial value for uncontrolled usage.
valuenumberControlled value. When provided, parent must handle onChange to update it.
minnumber0Minimum allowed value. Decrement button is disabled when value equals min.
maxnumber99Maximum allowed value. Increment button is disabled when value equals max.
stepnumber1Amount to increment or decrement on each button press.
onChange(value: number) => voidCalled with the new clamped value after each button press.
disabledbooleanfalseDisables both buttons.
labelstringVisible label above the counter. Required for accessibility when used without surrounding context.
idstringauto-generatedPassed to the label ID. Auto-generated with useId when omitted.
classNamestringAdditional 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

KeyAction
TabMove to decrement button
Tab againMove to increment button
Enter / SpaceActivate the focused button
  • 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.