Skip to main content

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

tsx
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/components

Import

Import the Input component into your file.

tsx
import { Input } from '@/components/aidash/input';

Usage

The simplest way to use Input with default props.

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

tsx
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" />
Tip
Use 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.

tsx
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

Success

Username is available!

Warning

Password is weak — consider adding numbers

tsx
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

tsx
1<Input2label="Full Name"3placeholder="John Doe"4helperText="Enter your first and last name"5/>

Prefix & Suffix Icons

tsx
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

https://
USD
tsx
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

tsx
1<Input2label="Display Name"3placeholder="Enter display name"4showCharCount5maxLength={30}6helperText="Visible to other users"7/>

Full Width & Disabled

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

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

PropTypeDefaultDescription
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)
labelstringLabel displayed above the input
helperTextstringHelper text shown below the input
errorTextstringError message (sets state to error style)
successTextstringSuccess message (sets state to success style)
warningTextstringWarning message (sets state to warning style)
prefixIconReactNodeIcon rendered inside the input on the left
suffixIconReactNodeIcon rendered inside the input on the right
prefixstringText prefix with a bordered inset area
suffixstringText suffix with a bordered inset area
showCharCountbooleanfalseShow a character count below the input
maxLengthnumberMaximum character limit (HTML + counter)
fullWidthbooleantrueStretch input to fill container width
clearablebooleanfalseShow a clear (X) button when the input has a value
onClear() => voidCallback fired when the clear button is clicked (also clears internal state for uncontrolled inputs)
containerClassNamestring''Additional CSS classes on the wrapper div
placeholderstringPlaceholder text (standard HTML)
disabledbooleanfalseDisable the input
typestring'text'HTML input type (text, email, password, etc.)
onChange(e: ChangeEvent) => voidChange handler
valuestringControlled value
classNamestring''Additional CSS classes on the input element
Note
Input extends 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

tsx
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

tsx
1<Input2variant="filled"3inputSize="lg"4placeholder="Search components, docs, examples..."5prefixIcon={<SearchIcon />}6/>

Settings Form

8/50

https://

Displayed on your public profile

0/160

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

Do
  • 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
Don't
  • 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.

src/components/aidash/input.tsx
tsx
'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>
  );
});

Other components that work well alongside Input.