Entrada
Un campo de formulario para capturar entrada de texto del usuario, con soporte para etiquetas, estados de validación y varios tipos de entrada.
Busca en todas las páginas de la documentación
Un campo de formulario para capturar entrada de texto del usuario, con soporte para etiquetas, estados de validación y varios tipos de entrada.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
interface InputProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export function Input({ label, value, onChange }: InputProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</label>
);
}Una entrada etiquetada mínima. La devolución de llamada onChange devuelve el valor de cadena directamente para que el componente padre no necesite desenvuelto e.target.value. Envolver la entrada dentro de un elemento <label> asocia la etiqueta con el campo sin necesidad de atributos htmlFor e id.
"use client";
interface InputProps {
label: string;
value: string;
onChange: (value: string) => void;
error?: string;
}
export function Input({ label, value, onChange, error }: InputProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
aria-invalid={!!error}
className={`mt-1 block w-full rounded-lg border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 ${
error
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
/>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
</label>
);
}Alterna los colores de borde y anillo a rojo cuando hay un mensaje de error presente. El atributo aria-invalid le indica a los lectores de pantalla que el campo tiene un problema de validación.
"use client";
interface InputProps {
label: string;
value: string;
onChange: (value: string) => void;
helperText?: string;
error?: string;
}
export function Input({ label, value, onChange, helperText, error }: InputProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
aria-invalid={!!error}
className={`mt-1 block w-full rounded-lg border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 ${
error
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
/>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
{!error && helperText && <p className="mt-1 text-sm text-gray-500">{helperText}</p>}
</label>
);
}El texto de ayuda aparece debajo de la entrada cuando no hay error. Cuando hay un error presente, toma prioridad, evitando que los mensajes conflictivos se apilen.
"use client";
import { useState } from "react";
interface PasswordInputProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export function PasswordInput({ label, value, onChange }: PasswordInputProps) {
const [visible, setVisible] = useState(false);
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<div className="relative mt-1">
<input
type={visible ? "text" : "password"}
value={value}
onChange={(e) => onChange(e.target.value)}
className="block w-full rounded-lg border border-gray-300 px-3 py-2 pr-10 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => setVisible((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-gray-400 hover:text-gray-600"
aria-label={visible ? "Ocultar contraseña" : "Mostrar contraseña"}
>
{visible ? (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-5 0-9.27-3.11-11-7.5a11.72 11.72 0 013.168-4.477M6.343 6.343A9.97 9.97 0 0112 5c5 0 9.27 3.11 11 7.5a11.7 11.7 0 01-4.373 5.157M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3l18 18" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-.274.857-.642 1.68-1.1 2.453M12 19c-1.39 0-2.72-.285-3.927-.8" />
</svg>
)}
</button>
</div>
</label>
);
}Alterna entre type="text" y type="password" con un botón de visibilidad. El botón de alternancia usa type="button" para evitar el envío del formulario y aria-label para comunicar el estado actual.
"use client";
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function SearchInput({ value, onChange, placeholder = "Buscar..." }: SearchInputProps) {
return (
<div className="relative">
<svg
className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="block w-full rounded-lg border border-gray-300 py-2 pl-10 pr-3 text-sm shadow-sm placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
);
}El icono de búsqueda se posiciona absolutamente dentro del contenedor de entrada. El relleno izquierdo (pl-10) evita que el texto se superponga al icono. Usar type="search" da a los navegadores botones de limpieza nativos en algunas plataformas.
"use client";
interface TextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
rows?: number;
maxLength?: number;
}
export function Textarea({ label, value, onChange, rows = 4, maxLength }: TextareaProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={rows}
maxLength={maxLength}
className="mt-1 block w-full resize-y rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
{maxLength && (
<p className="mt-1 text-right text-xs text-gray-400">
{value.length}/{maxLength}
</p>
)}
</label>
);
}Una entrada de múltiples líneas con contador de caracteres opcional. La clase resize-y permite cambiar el tamaño verticalmente solo, evitando problemas de desbordamiento horizontal. El contador se actualiza en vivo mientras el usuario escribe.
"use client";
import { forwardRef, useId, useState, useCallback } from "react";
type InputSize = "sm" | "md" | "lg";
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "onChange"> {
label?: string;
helperText?: string;
error?: string;
size?: InputSize;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
onValueChange?: (value: string) => void;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
fullWidth?: boolean;
}
const sizeClasses: Record<InputSize, { input: string; text: string }> = {
sm: { input: "h-8 px-2.5 text-xs", text: "text-xs" },
md: { input: "h-10 px-3 text-sm", text: "text-sm" },
lg: { input: "h-12 px-4 text-base", text: "text-base" },
};
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{
label,
helperText,
error,
size = "md",
leftIcon,
rightIcon,
onValueChange,
onChange,
fullWidth = true,
disabled,
className,
id: externalId,
...rest
},
ref
) {
const generatedId = useId();
const inputId = externalId ?? generatedId;
const errorId = `${inputId}-error`;
const helperId = `${inputId}-helper`;
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
onValueChange?.(e.target.value);
},
[onChange, onValueChange]
);
const hasError = !!error;
const sizes = sizeClasses[size];
return (
<div className={fullWidth ? "w-full" : "inline-flex flex-col"}>
{label && (
<label htmlFor={inputId} className={`mb-1 block font-medium text-gray-700 ${sizes.text}`}>
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
{leftIcon}
</span>
)}
<input
ref={ref}
id={inputId}
disabled={disabled}
onChange={handleChange}
aria-invalid={hasError}
aria-describedby={
[hasError ? errorId : null, helperText ? helperId : null].filter(Boolean).join(" ") || undefined
}
className={[
"block rounded-lg border shadow-sm transition-colors",
"focus:outline-none focus:ring-1",
"disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500",
"placeholder:text-gray-400",
sizes.input,
fullWidth ? "w-full" : "",
leftIcon ? "pl-10" : "",
rightIcon ? "pr-10" : "",
hasError
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500",
className ?? "",
]
.filter(Boolean)
.join(" ")}
{...rest}
/>
{rightIcon && (
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
{rightIcon}
</span>
)}
</div>
{hasError && (
<p id={errorId} className={`mt-1 text-red-600 ${sizes.text}`} role="alert">
{error}
</p>
)}
{!hasError && helperText && (
<p id={helperId} className={`mt-1 text-gray-500 ${sizes.text}`}>
{helperText}
</p>
)}
</div>
);
});Aspectos clave:
<label>, aria-describedby, y elementos de texto de error/ayuda sin requerir que el consumidor proporcione IDs.onChange nativo como una devolución de llamada de cadena simplificada onValueChange, haciéndolo compatible con bibliotecas de formularios y estado simple.disabled:cursor-not-allowed y un fondo atenuado para que el estado deshabilitado sea visualmente obvio sin depender solo de la opacidad.Advertencia de componente no controlado a controlado -- Comenzar con value={undefined} y luego cambiar a una cadena activa una advertencia de React. Siempre inicializa el estado como una cadena vacía, no undefined.
Falta de asociación id/htmlFor -- Usar un <label> que no está envolviendo la entrada y no está conectado vía htmlFor significa que hacer clic en la etiqueta no enfoca la entrada. Envuelve la entrada en la etiqueta o usa atributos id y htmlFor coincidentes.
onChange devuelve evento, no valor -- A diferencia de algunas bibliotecas de componentes, el onChange nativo te da un objeto de evento. Olvidar e.target.value es una fuente común de [object Object] que aparece en entradas.
Peculiaridades de type="number" -- onChange todavía devuelve una cadena con type="number". Usa parseFloat(e.target.value) y maneja NaN. Además, las entradas numéricas permiten caracteres e, +, -, y . que parseInt ignora silenciosamente.
Desajuste del teclado móvil -- Usar type="text" para campos de correo electrónico o teléfono muestra un teclado genérico en dispositivos móviles. Usa type="email", type="tel", o inputMode="numeric" para obtener el diseño de teclado correcto.
Conflictos de estilos de relleno automático -- El relleno automático del navegador aplica su propio color de fondo (generalmente amarillo o azul). Anula con autofill:bg-white autofill:shadow-[inset_0_0_0px_1000px_white] en Tailwind si es necesario.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥