Input
A flexible text input component with 3 variants, 4 validation states, 3 sizes, label and helper text support, prefix/suffix icons and text addons, and character counting. Built with forwardRef for seamless form library integration.
Live Preview
Interact with the Input component in real time. Adjust variant, state, and size to see changes instantly.
This is helper text
1<Input2variant="default"3state="default"4inputSize="md"5label="Label"6placeholder="Type something..."7 helperText="This is helper text"8/>Installation
Install the Aidash Components package using your preferred package manager.
$ pnpm add @aidash/componentsImport
Import the Input component into your file.
import { Input } from '@/components/aidash/input';Usage
The simplest way to use Input with default props.
1<Input placeholder="Enter your name" />2<Input label="Email" placeholder="you@example.com" type="email" />Variants
Three visual variants for different design contexts and surface treatments.
1<Input variant="default" label="Default" placeholder="Surface background with border" />2<Input variant="filled" label="Filled" placeholder="Sunken background, no visible border" />3<Input variant="outlined" label="Outlined" placeholder="Transparent background, 2px border" />default for standard forms, filled for inputs on elevated surfaces where a visible border would be too heavy, and outlined for inputs that need stronger visual emphasis.Sizes
Three sizes to fit different layout contexts, from compact to spacious.
1<Input inputSize="sm" label="Small" placeholder="Compact — h-8" />2<Input inputSize="md" label="Medium" placeholder="Default — h-9" />3<Input inputSize="lg" label="Large" placeholder="Spacious — h-11" />States
Four validation states with color-coded borders and feedback messages.
Default
Choose a unique username
Error
Please enter a valid email address
Success
Username is available!
Warning
Password is weak — consider adding numbers
1{/* Default with helper text */}2<Input label="Username" helperText="Choose a unique username" />3 4{/* Error state */}5<Input label="Email" state="error" errorText="Please enter a valid email address" />6 7{/* Success state */}8<Input label="Username" state="success" successText="Username is available!" />9 10{/* Warning state */}11<Input label="Password" state="warning" warningText="Password is weak" />Features
Advanced features including labels, icons, text addons, character counting, and more.
Label & Helper Text
Enter your first and last name
1<Input2label="Full Name"3placeholder="John Doe"4helperText="Enter your first and last name"5/>Prefix & Suffix Icons
1<Input label="Search" placeholder="Search..." prefixIcon={<SearchIcon />} />2<Input label="Email" placeholder="you@example.com" prefixIcon={<MailIcon />} />3<Input4label="Password"5type="password"6prefixIcon={<LockIcon />}7suffixIcon={<EyeIcon />}8/>Prefix & Suffix Text
1<Input label="Website" placeholder="example.com" prefix="https://" />2<Input label="Price" placeholder="0.00" suffix="USD" />Character Count
Visible to other users
0/30
1<Input2label="Display Name"3placeholder="Enter display name"4showCharCount5maxLength={30}6helperText="Visible to other users"7/>Full Width & Disabled
1{/* Full width is the default */}2<Input label="Full Width" placeholder="Stretches to fill container" />3 4{/* Inline mode */}5<Input label="Inline" placeholder="Inline input" fullWidth={false} />6 7{/* Disabled state */}8<Input label="Disabled" placeholder="Cannot type here" disabled />Accessibility
Built-in accessibility features for label association, error announcements, and keyboard navigation.
Label Association
When a label prop is provided, it is automatically linked to the input via htmlFor/id. A unique ID is generated if none is supplied.
Error Announcements
Error, success, and warning text are rendered below the input, providing visual feedback. Pair with aria-describedby for screen reader announcements in your form wrapper.
Keyboard Navigation
Standard Tab navigation. The input shows a brand-colored focus ring on focus. Disabled inputs are skipped in the tab order via the native disabled attribute.
Ref Forwarding
Supports forwardRef for programmatic focus, selection, and integration with form libraries like React Hook Form.
1{/* Label is automatically linked via htmlFor/id */}2<Input label="Email" placeholder="you@example.com" />3 4{/* Custom ID for manual aria-describedby */}5<Input id="username" label="Username" aria-describedby="username-hint" />6<p id="username-hint">Must be 3-20 characters</p>7 8{/* Ref forwarding for programmatic focus */}9const inputRef = useRef<HTMLInputElement>(null);10<Input ref={inputRef} label="Focus Me" />API Reference
Complete list of props accepted by Input.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'default' | 'filled' | 'outlined' | 'default' | Visual style of the input |
| state | 'default' | 'error' | 'success' | 'warning' | 'default' | Validation state with corresponding border color |
| inputSize | 'sm' | 'md' | 'lg' | 'md' | Size of the input (height, font, padding) |
| label | string | — | Label displayed above the input |
| helperText | string | — | Helper text shown below the input |
| errorText | string | — | Error message (sets state to error style) |
| successText | string | — | Success message (sets state to success style) |
| warningText | string | — | Warning message (sets state to warning style) |
| prefixIcon | ReactNode | — | Icon rendered inside the input on the left |
| suffixIcon | ReactNode | — | Icon rendered inside the input on the right |
| prefix | string | — | Text prefix with a bordered inset area |
| suffix | string | — | Text suffix with a bordered inset area |
| showCharCount | boolean | false | Show a character count below the input |
| maxLength | number | — | Maximum character limit (HTML + counter) |
| fullWidth | boolean | true | Stretch input to fill container width |
| clearable | boolean | false | Show a clear (X) button when the input has a value |
| onClear | () => void | — | Callback fired when the clear button is clicked (also clears internal state for uncontrolled inputs) |
| containerClassName | string | '' | Additional CSS classes on the wrapper div |
| placeholder | string | — | Placeholder text (standard HTML) |
| disabled | boolean | false | Disable the input |
| type | string | 'text' | HTML input type (text, email, password, etc.) |
| onChange | (e: ChangeEvent) => void | — | Change handler |
| value | string | — | Controlled value |
| className | string | '' | Additional CSS classes on the input element |
InputHTMLAttributes<HTMLInputElement> (with size omitted in favor of inputSize), so it also accepts all standard HTML input attributes such as placeholder, type, autoComplete, and aria-* props.Examples
Realistic production examples showing the Input in context.
Login Form
1<Input2label="Email"3type="email"4placeholder="you@example.com"5prefixIcon={<MailIcon />}6/>7<Input8label="Password"9type="password"10placeholder="Enter password"11prefixIcon={<LockIcon />}12/>Search Bar
1<Input2variant="filled"3inputSize="lg"4placeholder="Search components, docs, examples..."5prefixIcon={<SearchIcon />}6/>Settings Form
8/50
Displayed on your public profile
0/160
1<Input2label="Display Name"3prefixIcon={<UserIcon />}4showCharCount5maxLength={50}6/>7<Input8label="Website"9placeholder="example.com"10prefix="https://"11/>12<Input13label="Bio"14placeholder="A short bio"15showCharCount16maxLength={160}17helperText="Displayed on your public profile"18/>Best Practices
Guidelines for using inputs effectively in your interface.
- Always provide a label for screen readers and usability
- Use descriptive placeholder text that shows expected format
- Show validation feedback inline using errorText/successText
- Use appropriate input types (email, password, tel, url)
- Set maxLength with showCharCount for length-limited fields
- Use consistent sizing within the same form
- Use placeholder text as a substitute for labels
- Rely only on color to convey validation state
- Mix different input sizes within the same form
- Use both prefix text and prefixIcon at the same time
- Use Input for multi-line content (use Textarea instead)
- Validate on every keystroke without debouncing
Source Code
Full source code for the Input component.
'use client';
import { useState, useId, forwardRef, type InputHTMLAttributes, type ReactNode } from 'react';
import { HugeiconsIcon } from '@hugeicons/react';
import { Cancel01Icon } from '@hugeicons/core-free-icons';
import {
type InputVariant,
type InputState,
type InputSize,
variantStyles,
sizeStyles,
stateColors,
} from './input-shared';
export type { InputVariant, InputState, InputSize } from './input-shared';
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
variant?: InputVariant;
state?: InputState;
inputSize?: InputSize;
label?: string;
helperText?: string;
errorText?: string;
successText?: string;
warningText?: string;
prefixIcon?: ReactNode;
suffixIcon?: ReactNode;
prefix?: string;
suffix?: string;
showCharCount?: boolean;
maxLength?: number;
fullWidth?: boolean;
clearable?: boolean;
onClear?: () => void;
containerClassName?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{
variant = 'default',
state = 'default',
inputSize = 'md',
label,
helperText,
errorText,
successText,
warningText,
prefixIcon,
suffixIcon,
prefix,
suffix,
showCharCount = false,
maxLength,
fullWidth = true,
clearable = false,
onClear,
containerClassName = '',
className = '',
disabled,
value,
defaultValue,
onChange,
id,
...rest
},
ref
) {
const [internalValue, setInternalValue] = useState(defaultValue?.toString() || '');
const currentValue = value !== undefined ? value.toString() : internalValue;
const charCount = currentValue.length;
const showClear = clearable && charCount > 0 && !disabled;
const autoId = useId();
const inputId = id || `input-${autoId}`;
const feedbackId = `${inputId}-feedback`;
const activeState = disabled ? 'default' : state;
const feedbackText = errorText || successText || warningText || helperText;
const feedbackState = errorText ? 'error' : successText ? 'success' : warningText ? 'warning' : 'default';
const isInvalid = feedbackState === 'error' || activeState === 'error';
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (value === undefined) {
setInternalValue(e.target.value);
}
onChange?.(e);
};
const handleClear = () => {
if (value === undefined) {
setInternalValue('');
}
onClear?.();
};
return (
<div className={`${fullWidth ? 'w-full' : 'inline-flex flex-col'} ${containerClassName}`}>
{label && (
<label
htmlFor={inputId}
className={`block font-medium text-(--text) mb-1.5 ${sizeStyles[inputSize].label}`}
>
{label}
</label>
)}
<div className="relative flex items-center">
{prefix && (
<span className="absolute left-0 flex items-center justify-center h-full px-3 text-(--text-muted) text-sm border-r border-(--border) bg-(--surface-sunken) rounded-l-lg">
{prefix}
</span>
)}
{prefixIcon && !prefix && (
<span className="absolute left-3 flex items-center text-(--text-muted)">
{prefixIcon}
</span>
)}
<input
ref={ref}
id={inputId}
value={value}
defaultValue={value === undefined ? defaultValue : undefined}
onChange={handleChange}
maxLength={maxLength}
disabled={disabled}
aria-invalid={isInvalid || undefined}
aria-describedby={feedbackText ? feedbackId : undefined}
className={[
'w-full rounded-lg outline-none transition-colors text-(--text)',
variantStyles[variant][activeState],
sizeStyles[inputSize].input,
disabled ? 'opacity-50 cursor-not-allowed' : '',
prefixIcon && !prefix ? 'pl-10' : '',
prefix ? 'pl-16' : '',
suffixIcon || (showCharCount && maxLength) ? 'pr-10' : '',
showClear && !suffixIcon ? 'pr-9' : '',
showClear && suffixIcon ? 'pr-16' : '',
suffix ? 'pr-16' : '',
className,
].filter(Boolean).join(' ')}
{...rest}
/>
{showClear && (
<button
type="button"
onClick={handleClear}
aria-label="Clear input"
className={`absolute ${suffixIcon ? 'right-9' : 'right-2'} p-1 rounded-md text-(--text-muted) hover:text-(--text) hover:bg-(--surface-sunken) transition-colors focus-visible:outline-2 focus-visible:outline-(--ring) focus-visible:outline-offset-2`}
>
<HugeiconsIcon icon={Cancel01Icon} size={14} aria-hidden="true" />
</button>
)}
{suffixIcon && (
<span className="absolute right-3 flex items-center text-(--text-muted)">
{suffixIcon}
</span>
)}
{suffix && (
<span className="absolute right-0 flex items-center justify-center h-full px-3 text-(--text-muted) text-sm border-l border-(--border) bg-(--surface-sunken) rounded-r-lg">
{suffix}
</span>
)}
</div>
<div className="flex items-center justify-between mt-1.5 gap-2">
{feedbackText && (
<p
id={feedbackId}
role={feedbackState === 'error' ? 'alert' : undefined}
className={`${sizeStyles[inputSize].helper} ${stateColors[feedbackState]}`}
>
{feedbackText}
</p>
)}
{showCharCount && maxLength && (
<p className={`${sizeStyles[inputSize].helper} text-(--text-muted) ml-auto tabular-nums`}>
{charCount}/{maxLength}
</p>
)}
</div>
</div>
);
});Related
Other components that work well alongside Input.