Input

An enhanced input component that wraps the native HTML input with support for a visible label, helper description, error message, and left/right icon slots. Accessibility wiring (id, aria-describedby, aria-invalid) is handled automatically via React's useId hook.

Installation

npm install @designforge/ui

Usage

import { Input } from "@/components/ui/input";

Examples

Basic

import { Input } from "@/components/ui/input";
 
export function BasicInput() {
  return <Input placeholder="Enter text…" />;
}

With Label and Description

import { Input } from "@/components/ui/input";
 
export function WithLabelAndDescription() {
  return (
    <Input
      label="Username"
      description="Only letters, numbers, and underscores are allowed."
      placeholder="e.g. mayank_dev"
    />
  );
}

Error State

import { Input } from "@/components/ui/input";
 
export function ErrorState() {
  return (
    <Input
      label="Email address"
      type="email"
      defaultValue="not-an-email"
      error="Please enter a valid email address."
    />
  );
}

Left Icon

import { Mail } from "lucide-react";
import { Input } from "@/components/ui/input";
 
export function LeftIconInput() {
  return (
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      leftIcon={<Mail className="h-4 w-4" />}
    />
  );
}

Right Icon

import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
 
export function RightIconInput() {
  return (
    <Input
      placeholder="Search components…"
      rightIcon={<Search className="h-4 w-4" />}
    />
  );
}

Password Toggle

import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
import { Input } from "@/components/ui/input";
 
export function PasswordToggle() {
  const [visible, setVisible] = useState(false);
 
  return (
    <Input
      label="Password"
      type={visible ? "text" : "password"}
      placeholder="Enter your password"
      rightIcon={
        <button
          type="button"
          onClick={() => setVisible((v) => !v)}
          className="text-muted-foreground hover:text-foreground"
          aria-label={visible ? "Hide password" : "Show password"}
        >
          {visible ? (
            <EyeOff className="h-4 w-4" />
          ) : (
            <Eye className="h-4 w-4" />
          )}
        </button>
      }
    />
  );
}

Search Input

import { Search, X } from "lucide-react";
import { useState } from "react";
import { Input } from "@/components/ui/input";
 
export function SearchInput() {
  const [value, setValue] = useState("");
 
  return (
    <Input
      placeholder="Search…"
      value={value}
      onChange={(e) => setValue(e.target.value)}
      leftIcon={<Search className="h-4 w-4" />}
      rightIcon={
        value ? (
          <button
            type="button"
            onClick={() => setValue("")}
            className="text-muted-foreground hover:text-foreground"
            aria-label="Clear search"
          >
            <X className="h-4 w-4" />
          </button>
        ) : null
      }
    />
  );
}

Disabled

import { Input } from "@/components/ui/input";
 
export function DisabledInput() {
  return (
    <Input
      label="Account ID"
      value="acc_1A2B3C4D5E"
      disabled
      description="Your account ID cannot be changed."
    />
  );
}

Form with Validation

import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
 
interface FormErrors {
  name?: string;
  email?: string;
}
 
export function FormWithValidation() {
  const [values, setValues] = useState({ name: "", email: "" });
  const [errors, setErrors] = useState<FormErrors>({});
 
  function validate(): FormErrors {
    const errs: FormErrors = {};
    if (!values.name.trim()) errs.name = "Name is required.";
    if (!values.email.includes("@")) errs.email = "Enter a valid email address.";
    return errs;
  }
 
  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const errs = validate();
    if (Object.keys(errs).length > 0) {
      setErrors(errs);
      return;
    }
    setErrors({});
    alert("Form submitted!");
  }
 
  return (
    <form onSubmit={handleSubmit} className="space-y-4 w-80">
      <Input
        label="Full Name"
        placeholder="Mayank Kumar"
        value={values.name}
        onChange={(e) => setValues((v) => ({ ...v, name: e.target.value }))}
        error={errors.name}
        required
      />
      <Input
        label="Email"
        type="email"
        placeholder="you@example.com"
        value={values.email}
        onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
        error={errors.email}
        required
      />
      <Button type="submit" className="w-full">
        Submit
      </Button>
    </form>
  );
}

API Reference

Input

Extends all standard HTML <input> attributes. The following props are added by the component.

PropTypeDefaultDescription
labelstringVisible <label> text rendered above the input. Automatically linked via the auto-generated id.
descriptionstringHelper text rendered below the input. Added to aria-describedby automatically.
errorstringError message rendered below the input (replaces description). Sets aria-invalid="true" and aria-describedby on the input.
leftIconReactNodeNode rendered inside the input on the leading (left) side. The input gains left padding to avoid overlap.
rightIconReactNodeNode rendered inside the input on the trailing (right) side. The input gains right padding to avoid overlap.
wrapperClassNamestringAdditional CSS classes applied to the outer wrapper <div> that contains the label, input, and helper text.
classNamestringAdditional CSS classes applied directly to the <input> element.

Automatically managed attributes

AttributeBehaviour
idAuto-generated with React's useId hook. Override by passing a manual id.
aria-describedbySet to the id of the description or error element when either is present.
aria-invalidSet to "true" when error is non-empty.

All other props (type, placeholder, value, onChange, disabled, required, autoComplete, etc.) are forwarded directly to the underlying <input> element.

Accessibility

  • The label prop renders a real <label> element associated with the input via matching id / htmlFor attributes, ensuring screen readers announce the label when the input receives focus.
  • When description or error is provided, the input's aria-describedby is set automatically so assistive technologies read the supplementary text after the label.
  • An error prop also sets aria-invalid="true", which communicates an invalid state to screen readers without relying solely on colour.
  • Error messages are visually styled in red and programmatically associated — never convey errors through colour alone.
  • Use disabled for read-only locked fields. Disabled inputs are removed from the tab order; if the value must remain readable and focusable, use readOnly instead.
  • Icon slots accept arbitrary ReactNode. When an icon carries meaning (e.g., a clear button), ensure it has an accessible aria-label or visually hidden text.

Live View

Open in Storybook ↗