Input

A compound text field with a label, optional hint, prefix icons, suffix icons, prefix labels, and interactive suffix buttons. Built with slots and Context - each part is a named export.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add input

Usage#

usage.tsxTSX
import {  InputRoot,  InputField,  InputLabel,} from "@/components/ui/input"export function Example() {  return (    <div className="flex flex-col gap-1.5">      <InputLabel htmlFor="email">Email</InputLabel>      <InputRoot>        <InputField id="email" placeholder="you@example.com" type="email" />      </InputRoot>    </div>  )}

Anatomy#

import {  InputLabel,  InputRoot,  InputPrefix,  InputPrefixLabel,  InputField,  InputSuffix,  InputSuffixButton,  InputHint,} from "@/components/ui/input"<InputLabel /><InputRoot>  <InputPrefix />  <InputPrefixLabel />  <InputField />  <InputSuffix />  <InputSuffixButton /></InputRoot><InputHint />

Playground#

The props dress the shell; the parts are everything around the field. A label, a hint, a leading icon and a trailing slot are all children you compose, so each one you switch off disappears from the code.

Lowercase letters and dashes.

Playground
Props

A bordered shell, or a bare inline field.

Height comes from size alone, never from density.

Destructive ring, and the hint turns with it.

Dims the shell and blocks input.

Parts

The field's name, wired by htmlFor.

A leading icon inside the shell.

A trailing slot inside the shell.

Helper text under the field.

input.tsxTSX
<div className="w-full max-w-xs space-y-1.5">  <InputLabel htmlFor="playground">Workspace</InputLabel>  <InputRoot>    <InputField id="playground" placeholder="koala-design" />  </InputRoot>  <InputHint>    <Component swapKey="idle" align="start">      Lowercase letters and dashes.    </Component>  </InputHint></div>

Label & hint#

InputLabel sits above the control - wire it to the field with htmlFor pointing at the InputField’s id so clicking the label focuses the input. Pass required to append a destructive asterisk. InputHint renders helper text below; give it an id and reference it from the field’s aria-describedby.

Used in your team URL. Lowercase letters and dashes only.

Prefix icon#

Use InputPrefix to place a decorative icon on the left. The slot sizes any svg automatically and marks it aria-hidden so it stays invisible to assistive tech.

Suffix icon#

Use InputSuffix to place a decorative icon on the right - useful for status indicators or type hints.

Prefix label#

InputPrefixLabel renders a fixed text segment flush against the left border - ideal for URL schemes, currency symbols, or units. It uses a negative margin to reach the container edge and overflow-hidden on the root clips it at the rounded corner.

https://

Suffix button#

InputSuffixButton renders an interactive ghost button inside the field - e.g. a clear action. Always pass aria-label since there is no visible text.

Password#

PasswordInput is a convenience wrapper that manages the show/hide toggle internally. It renders InputRoot + InputField + InputSuffixButton with an Eye / EyeSlash icon from Koala Icons. Pass rootClassName to style the wrapper, and all other props go to the underlying input element.

Number#

NumberInput adds stepper controls that clamp to min/max, snap to step, and respond to ↑/↓ keys, click, and press-and-hold to repeat. The default stacked carets sit on the right; layout="inline" gives a − / + quantity stepper with full-height hit targets.

Quantity stepper#

The layout="inline" NumberInput is the − value + quantity stepper: compact, clamped at min, and keyboard- and hold-to-repeat friendly. It's the control behind a cart row's OrderSummaryItemQuantity (see Order Summary → Shopping cart). Give it a fixed rootClassName width so it stays tidy in a row.

Phone#

PhoneInput pairs the field with a searchable country picker - circular flags, type-to-filter, full keyboard navigation, and a live +code prefix. onChange reports a { country, dialCode, nationalNumber, e164 } payload; store e164 as the canonical value. The country segment takes the field's resolved size, so inside a Form it grows with the other controls and its hover fills flush to the border.

E.164: +15550142

Sizes#

Three sizes on a 4px grid - sm (32 px), md (36 px), and lg (40 px). Unset, it follows the container or the density (sm under the default compact). Set size on InputRoot; all parts pick it up from context.

Error#

Pass hasError to InputRoot to switch the border and focus ring to the destructive color and set aria-invalid on the underlying input. Pass the same hasError to InputHint so the helper text doubles as the error message.

Enter a valid email address.

Disabled#

Inline#

Pass variant="inline" to InputRoot for a chromeless field: the border and fill stay invisible at rest and the container only materializes on hover, while focus keeps the usual brand ring. Perfect for edit-in-place UI - editable titles, table cells, or settings rows that should read as plain text until you reach for them.

Floating label#

Swap InputField for InputFloatingField and the label lives inside the frame. It rests where the placeholder would and, the moment you click in, shrinks and rises to the top edge, so the label never disappears while you type. A field that already holds a value renders floated from the first paint.

Only lowercase letters, numbers and dashes.

API reference#

InputRoot

The bordered frame that owns size, error and disabled state and shares them with its parts. Renders a <div> and forwards all its props. Every part accepts className, merged last.

PropTypeDefaultDescription
size"sm" | "md" | "lg"-Height 32, 36 or 40px. Unset, it comes from a container (Form imposes lg) or density (compact, the default, is sm; comfortable is md).
variant"default" | "inline""default"inline is chromeless at rest and shows its frame on hover and focus, for inline-edit fields.
hasErrorboolean-Destructive border and ring, and aria-invalid on the input. Inherited from a surrounding Field when unset.
disabledboolean-Dims and locks the frame and disables the input. Inherited from a surrounding Field.
density"comfortable" | "compact"-Only picks the default size. An explicit size wins.
Data attributeDescription
data-disabledPresent when disabled.
data-errorPresent in the error state.

InputField

The <input> itself; forwards all its props except size. Inside a Field it takes the field's id and aria-describedby.

InputLabel

The shared Label, re-exported. Place it outside InputRoot and point htmlFor at the field's id.

PropTypeDefaultDescription
requiredboolean-Appends a destructive asterisk and a screen-reader (required).
disabledboolean-Dims the label.

InputHint

The shared Hint, re-exported: helper or error text below the frame. Renders a <p>.

PropTypeDefaultDescription
hasErrorboolean-Switches to the destructive tone, so the hint reads as the error.

InputFloatingField

An <input> with its label inside the frame: the label rests where the placeholder would and floats to the top edge on focus, on a value or on autofill. Pure CSS, so defaultValue, controlled values and autofill all render floated with no animation on load. Use it in place of InputField; the frame grows to 44, 48 or 56px. Other props go to the <input>.

PropTypeDefaultDescription
labelReact.ReactNode-The floating label, tied to the input by htmlFor.
placeholderstring-Shown only once the label has floated and the focused field is still empty.
labelClassNamestring-Merged onto the <label>; className goes to the <input>.

InputPrefix

A leading decorative icon, aria-hidden. Renders a <span>.

InputSuffix

A trailing decorative icon, aria-hidden. Renders a <span>.

InputPrefixLabel

A divided leading label inside the frame (https://, $). Renders a <span>.

InputSuffixButton

A trailing icon button inside the frame (clear, copy). Renders a <button type="button">; pass aria-label, since it has no visible text.

PasswordInput

InputRoot, InputField and a show/hide InputSuffixButton in one. Other props go to the <input>.

PropTypeDefaultDescription
size"sm" | "md" | "lg"-Height 32, 36 or 40px. Unset, it comes from a container (Form imposes lg) or density (compact, the default, is sm; comfortable is md).
variant"default" | "inline""default"As on InputRoot.
hasErrorboolean-Destructive border and ring, and aria-invalid on the input. Inherited from a surrounding Field when unset.
density"comfortable" | "compact"-As on InputRoot.
rootClassNamestring-Merged onto the wrapping InputRoot; className goes to the <input>.

NumberInput

A numeric field with steppers and arrow-key increments. Other props go to the <input>.

PropTypeDefaultDescription
valuenumber | null-Controlled value; null for empty.
defaultValuenumber | null-Initial value, uncontrolled.
onValueChange(value: number | null) => void-Called on every committed change with the parsed number, or null when cleared.
minnumber-Lower clamp.
maxnumber-Upper clamp.
stepnumber1Applied by the steppers and arrow keys.
layout"stacked" | "inline""stacked"stacked carets on the right, or inline minus and plus flanking the field.
size"sm" | "md" | "lg"-Height 32, 36 or 40px. Unset, it comes from a container (Form imposes lg) or density (compact, the default, is sm; comfortable is md).
hasErrorboolean-Destructive border and ring, and aria-invalid on the input. Inherited from a surrounding Field when unset.
staticboolean-Kept for compatibility; controls no longer scale on press.
rootClassNamestring-Merged onto the wrapping InputRoot; className goes to the <input>.

PhoneInput

A country picker with dial code and a national-number field. Other props go to the <input>.

PropTypeDefaultDescription
countrystring-Controlled country, an ISO 3166-1 alpha-2 code.
defaultCountrystring"US"Initial country, uncontrolled.
onCountryChange(iso2: string) => void-Called when another country is picked.
valuestring-Controlled national number.
defaultValuestring-Initial national number, uncontrolled.
onValueChange(nationalNumber: string) => void-Called with the national number as typed.
onChange(payload: PhoneChangePayload) => void-Called on any change with { country, dialCode, nationalNumber, e164 }. Store e164.
placeholderstring"Phone number"The field's placeholder.
size"sm" | "md" | "lg"-Height 32, 36 or 40px. Unset, it comes from a container (Form imposes lg) or density (compact, the default, is sm; comfortable is md).
hasErrorboolean-Destructive border and ring, and aria-invalid on the input. Inherited from a surrounding Field when unset.
rootClassNamestring-Merged onto the wrapping InputRoot; className goes to the <input>.

FAQ#

`InputRoot` is the bordered container that owns the size, error, and disabled state and shares it via context, while `InputField` is the actual `<input>`. Splitting them lets `InputPrefix`, `InputSuffix`, and buttons sit inside the same border as siblings of the field.