Components
Input
StableInput fields let people enter text, choose from a list, or write longer notes. A unified component covers single-line text, select dropdowns, and textareas.
Purpose
The Input component is the primary data-entry surface across every Kuoni form — traveller details, search widgets, budget fields, notes sections, and dropdown pickers. A single component covers three element types through the as prop:
as='input'(default) — single-line free text; covers names, emails, passport numbers, dates.as='select'— a styled dropdown for a fixed set of options; use when choices are fewer than ~15 items and exhaustive.as='textarea'— multi-line text for longer, open-ended content like special requests or notes.
Inputs must always carry a visible label. Placeholders alone are not sufficient — they disappear the moment someone types, and they fail minimum contrast ratios. Every error must explain both what went wrong and how to fix it.
Usage
Reach for Input whenever a form needs free text, a fixed dropdown choice, or a longer note — it is the default data-entry building block. For a calendar date specifically, use DatePicker instead of type="date", since it gives a consistent cross-browser calendar UI. For a small bounded numeric count (traveller numbers, rooms), use CounterInput rather than a free-text number field.
Input appears throughout booking forms, search widgets, account settings, and any other data-entry surface in the product.
Anatomy
We send confirmations here.
- 1Label — Persistent text above the field. Tied to the input via htmlFor/id so clicking it focuses the field.
- 2Prefix — Optional text adornment inside the left edge of the field — e.g. "£" for currency, "+44" for phone.
- 3Left icon slot — Optional 14–16 px icon at the left of the field — e.g. a search magnifier.
- 4Input field — The editable area. Height is fixed at 44 px for comfortable touch targets.
- 5Right icon slot — Optional icon at the right. For select, always replaced by a chevron.
- 6Suffix — Optional text adornment inside the right edge — e.g. "pp" for per-person pricing.
- 7Help text — Guidance shown below the field when there is no error.
- 8Error text — Validation message with warning icon. Turns the border red and announces via aria-describedby.
Variants
Text input
The default element. Use for names, email addresses, passport numbers, search queries, and any other short free-form text.
Select (dropdown)
Use as='select' and pass <option> elements as children. The chevron icon is always rendered; iconRight is ignored for select.
When to use select: The set of choices is fixed and complete. Fewer than ~15 options. A free-text fallback does not make sense (e.g. country codes, room types, number of nights).
Textarea
Use as='textarea' for open-ended content such as special requests, dietary notes, or feedback. The field resizes vertically. Default row count is 4; customise with rows.
With left icon
Pass any React node (typically a 14–16 px Lucide icon) to iconLeft. Common uses: search fields, date pickers, email fields.
With prefix and suffix
prefix and suffix render as bordered text adornments attached to the field. Use for units, currency symbols, or domain suffixes.
States
Default and focus
The border moves from #CCCCCC (grey-300) to a 2 px teal ring (#005B55) on focus. The ring has zero offset so it sits flush with the border.
Error state
When validation fails, the border turns red, a warning icon appears beside the error message, and aria-invalid="true" is set on the field. Always write a message that explains how to fix the problem.
Enter a valid email address, for example jane@example.com.
Disabled
Disabled fields show a grey background and 60% opacity. They are excluded from tab order and cannot be edited.
Required
The required prop adds a red asterisk after the label (hidden from screen readers) and injects a visually-hidden "(required)" text for assistive technology. The native required attribute is also set for browser validation.
Sizes
All single-line inputs and selects use a fixed height of 44 px (h-11). This meets the WCAG 2.1 AA 44 × 44 px minimum touch-target size and creates visual consistency across forms. Textareas use auto height based on the rows prop.
| Element | Height | Touch target |
|---|---|---|
input | 44 px | ✅ WCAG AA |
select | 44 px | ✅ WCAG AA |
textarea | Auto (rows × line-height) | Resizable by user |
Best practices
Always provide a visible label above every input field.
A persistent label stays visible while the user types.
Use placeholder text as the only label for a field.
The placeholder disappears on input and fails contrast requirements.
Write error messages that name the issue and describe the fix: "Enter a valid email address."
Specific messages like this tell users exactly how to fix the problem.
Use vague error messages like "Invalid input" or "Error."
The user knows the input is invalid — they need to know why and what to do.
Use select for a fixed, exhaustive list of fewer than 15 options.
Select is scannable and faster than typing when choices are fixed.
Use a text input when only specific values are valid (e.g. country, number of travellers).
Typing a country code is error-prone and slow compared to a dropdown.
Use prefix/suffix for currency, units, or domain hints (£, pp, @domain.com).
Prefix and suffix reduce ambiguity without breaking the field layout.
Encode units in the placeholder text where they will disappear on input.
Mixing label text and units inside the placeholder creates confusion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | 'input' | 'select' | 'textarea' | 'input' | HTML element to render. Use select for dropdowns, textarea for multi-line text. |
| label | string | — | Visible label above the field. Tied to the input via htmlFor/id. |
| id | string | — | HTML id. Auto-derived from label (kebab-cased) when omitted. |
| type | string | 'text' | Native input type: 'text', 'email', 'password', 'number', 'tel', 'date', etc. Ignored for select and textarea. |
| placeholder | string | — | Hint text shown when the field is empty. Supplement to — not replacement of — the label. |
| helpText | string | — | Guidance text shown below the field. Hidden when error is set. |
| error | string | — | Validation message. Triggers red border, warning icon, and aria-invalid="true". |
| required | boolean | false | Marks the field as required. Adds red asterisk (visual) and "(required)" text (screen reader). |
| disabled | boolean | false | Disables the field. Applies grey background and 60% opacity. |
| iconLeft | ReactNode | — | Icon node rendered at the left edge of the field. Recommend 14–16 px Lucide icons. |
| iconRight | ReactNode | — | Icon node rendered at the right edge. Ignored when as="select" (chevron is always shown instead). |
| prefix | string | — | Text adornment attached to the left edge of the field. Mutually exclusive with iconLeft. |
| suffix | string | — | Text adornment attached to the right edge of the field. Mutually exclusive with iconRight. |
| rows | number | 4 | Number of visible text rows for textarea. Ignored for input and select. |
| children | ReactNode | — | Option elements for as="select". Pass <option> nodes as children. |
| className | string | — | Additional Tailwind classes on the wrapper div. |
Accessibility
Label association
Every Input must have a label prop. The component connects the label to the field via htmlFor / id automatically. Clicking the label focuses the input — important for small touch targets.
Never omit a label in favour of a placeholder. Placeholders disappear on input and are excluded from most screen-reader navigation modes.
Error announcement
When error is set:
aria-invalid="true"is applied to the field element.aria-describedbypoints to the error paragraph element.- The error paragraph has
role="alert"so assistive technology announces the message immediately when it appears.
Required fields
The required prop applies both the native required attribute (triggering browser validation) and injects a visually-hidden "(required)" text for screen readers. The red asterisk is marked aria-hidden="true" to avoid double-announcement.
Indicate at the top of long forms that asterisked fields are required: "Fields marked * are required."
Keyboard interaction
| Key | Action |
|---|---|
Tab | Move focus to the next interactive element |
Shift + Tab | Move focus to the previous element |
Enter / Space | Open select dropdown (browser native) |
| Arrow keys | Navigate select options when open |
Colour and contrast
Error state relies on both the red border colour and the error message text — never colour alone. The error text uses #C0392B (5.4:1 contrast on white — WCAG AA) and includes a warning icon for redundancy.
Related components
- DatePicker — use for calendar date selection instead of a native date input.
- CounterInput — use for small bounded numeric counts instead of a free-text number field.