Text Input
ProntoCampo de texto curto. Sem dependências externas. Suporta máscara, validação inline, sufixo/prefixo, botão clear e estados de sucesso/erro.
Usar quando
Captura de string única (nome, email, busca, OAB, CPF, CEP). Use sempre dentro de <label htmlFor>.
Não usar quando
Texto longo (use Textarea). Múltiplas opções (use Select/Radio). Senha sem visibilidade (sempre adicione toggle eye).
Variantes
Validação inline
Erro aparece on-blur. Sucesso aparece se passou. Ambos com microinteração.
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Border focus | focus | Borda do input passa de border para borderInk + outline brass-edge | 150ms ease |
| Label float (não usamos) | — | Label sempre acima (Apple-style). Sem floating label. | — |
| Success morph | validação on-blur passou | Ícone ✓ aparece à direita com fade + scale 0.8→1 | 250ms ease in |
| Error shake (sutil) | submit com erro de validação | Translação ±3px horizontal 2 vezes | 400ms total (4 × 100ms) |
| Clear button | value !== "" | Botão X aparece com fade-in à direita | 150ms ease |
| Mask format | onChange | Reformata enquanto digita (ex: 123456 → 123.456.789-00) | instant (no input event) |
| Suggestion popup | typo detectado on-blur (ex: gmail.con) | Hint sob input com sugestão clicável | 250ms ease in |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o input |
| Enter | Submete o form pai (se único input ou último) |
| Esc | Limpa o input (se houver clear button) |
| Ctrl/Cmd+A | Seleciona todo o conteúdo |
ARIA esperado
- <label htmlFor="email">E-mail</label> // sempre
- aria-invalid="true" // quando error
- aria-describedby="email-hint email-error" // liga hint e erro
- aria-required="true" // se obrigatório
- autoComplete="email" // hint para password manager
Notas
- Mensagem de erro NUNCA é só cor — sempre texto inline, com ícone.
- Placeholder NÃO substitui label. Cinza claro de placeholder não passa contraste AA.
- Inputs de senha sempre têm toggle de visibilidade (Eye/EyeOff).
- Inputs de busca devem ter type="search" para keyboard mobile mostrar tecla "Buscar".
- Inputs numéricos: use type="number" + inputMode="numeric" + pattern="[0-9]*".
Código
'use client';
import { useState } from 'react';
import { Search } from 'lucide-react';
import { T, TYPE, WEIGHT, SP, RADIUS, MOTION, transition } from '@/lib/tokens';
export function Input({
value,
onChange,
placeholder,
prefix,
suffix,
error,
success,
disabled,
readOnly,
}: Props) {
const [focused, setFocused] = useState(false);
const showError = !!error;
const showSuccess = success && !showError;
return (
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: SP[2],
background: disabled ? T.surface2 : T.surface,
border: `1px solid ${
showError ? T.carmim : focused ? T.borderInk : T.border
}`,
borderRadius: RADIUS.md,
paddingLeft: prefix ? SP[2] : SP[3],
paddingRight: suffix || showSuccess ? SP[2] : SP[3],
height: 36,
transition: transition('border-color', 'fast'),
outline: focused ? `2px solid ${T.brassEdge}` : 'none',
outlineOffset: 0,
}}
>
{prefix && <span style={{ color: T.inkSubtle }}>{prefix}</span>}
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
aria-invalid={showError}
style={{
flex: 1,
border: 'none',
outline: 'none',
background: 'transparent',
fontSize: TYPE.base,
color: T.ink,
fontFamily: 'inherit',
}}
/>
{showSuccess && <Check size={14} color={T.verde} />}
{suffix}
</div>
{error && (
<p style={{ marginTop: 4, fontSize: TYPE.sm, color: T.carmim }}>{error}</p>
)}
</div>
);
}Regras
Faça
- ✓Sempre acompanhe de <label htmlFor> visível acima.
- ✓Validação on-blur (não on-change) para não punir digitação parcial.
- ✓Erro inline com texto + ícone, nunca só cor.
- ✓Sucesso é opcional (use só em campos críticos: email, OAB, CNJ).
- ✓Use máscaras para CPF/CNPJ/CEP/CNJ — usuário cola sem formatação.
- ✓Sugestão de correção quando detectar typo conhecido (ex: gmail.con → gmail.com).
Não faça
- ✗Não use placeholder como label.
- ✗Não valide on-change (interrompe digitação).
- ✗Não use cor sem texto para erro (a11y fail).
- ✗Não esconda o label depois que o usuário começou a digitar (floating label).
- ✗Não force CAPS LOCK (pra OAB/CPF, deixe o usuário digitar minúsculo).
- ✗Não use border colorida para focus — use outline (não desloca layout).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background |
T.surface2 | #FAF8F2 | background quando disabled |
T.border | #E3DFD2 | borda default |
T.borderInk | #000000 | borda quando focus |
T.brassEdge | rgba(164,124,43,0.30) | outline ring no focus |
T.carmim | #A32E2E | borda + texto quando error |
T.verde | #2F6B3C | ícone quando success |
RADIUS.md | 12 | borderRadius |
TYPE.base | 13 | fontSize do conteúdo |
TYPE.sm | 12 | fontSize do hint/error |
MOTION.fast | 150ms | transition de border |