Input
Um campo de formulário para capturar a entrada de texto do usuário, suportando rótulos, estados de validação e vários tipos de entrada.
Busque em todas as páginas da documentação
Um campo de formulário para capturar a entrada de texto do usuário, suportando rótulos, estados de validação e vários 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>
);
}Uma entrada rotulada mínima. O callback onChange retorna o valor da string diretamente para que o pai não precise desempacotar e.target.value. Envolver a entrada dentro de um elemento <label> associa o rótulo ao campo sem a necessidade 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 as cores da borda e do anel para vermelho quando uma mensagem de erro está presente. O atributo aria-invalid informa aos leitores de tela que o campo tem um problema de validação.
"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>
);
}O texto de ajuda aparece abaixo da entrada quando não há erro. Quando um erro está presente, ele tem prioridade, evitando que mensagens conflitantes se empilhem.
"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 ? "Hide password" : "Show password"}
>
{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" e type="password" com um botão de visibilidade. O botão de alternância usa type="button" para evitar o envio do formulário e aria-label para comunicar o estado atual.
"use client";
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function SearchInput({ value, onChange, placeholder = "Search..." }: 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>
);
}O ícone de pesquisa é posicionado absolutamente dentro do contêiner da entrada. O preenchimento esquerdo (pl-10) evita que o texto se sobreponha ao ícone. Usar type="search" fornece botões de limpeza nativos em alguns navegadores.
"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>
);
}Uma entrada de várias linhas com contagem de caracteres opcional. A classe resize-y permite apenas o redimensionamento vertical, evitando problemas de estouro horizontal. O contador é atualizado em tempo real conforme o usuário digita.
"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 chave:
<label>, aria-describedby e texto de erro/ajuda sem exigir que o consumidor forneça IDs.onChange nativo quanto um callback de string onValueChange simplificado, tornando-o compatível com bibliotecas de formulário e estado simples.disabled:cursor-not-allowed e um fundo suave para que o estado desabilitado seja visualmente óbvio sem depender apenas da opacidade.Aviso de não controlado para controlado -- Começar com value={undefined} e depois mudar para uma string aciona um aviso do React. Sempre inicialize o estado como uma string vazia, não undefined.
Associação id/htmlFor ausente -- Usar um <label> que não está envolvendo a entrada e não está conectado via htmlFor significa que clicar no rótulo não foca a entrada. Envolva a entrada no rótulo ou use atributos id e htmlFor correspondentes.
onChange retorna evento, não valor -- Ao contrário de algumas bibliotecas de componentes, o onChange nativo fornece um objeto de evento. Esquecer e.target.value é uma fonte comum de [object Object] aparecendo nas entradas.
Quirks de type="number" -- onChange ainda retorna uma string com type="number". Use parseFloat(e.target.value) e lide com NaN. Além disso, entradas numéricas permitem caracteres e, +, - e . que parseInt ignora silenciosamente.
Conflitos de estilo de preenchimento automático -- O preenchimento automático do navegador aplica sua própria cor de fundo (geralmente amarela ou azul). Substitua com autofill:bg-white autofill:shadow-[inset_0_0_0px_1000px_white] no Tailwind, se necessário.
Teclado móvel incompatível -- Usar type="text" para campos de e-mail ou telefone exibe um teclado genérico no celular. Use type="email", type="tel" ou inputMode="numeric" para obter o layout correto do teclado.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥