Text Input

Pronto

Campo 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

Use o e-mail de cadastro na OAB.

Validação inline

Erro aparece on-blur. Sucesso aparece se passou. Ambos com microinteração.

Estados

Default
Hover (passe o mouse)
Focus (Tab)
Filled
Success
Error
E-mail inválido.
Disabled
Read-only

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Border focusfocusBorda do input passa de border para borderInk + outline brass-edge150ms ease
Label float (não usamos)Label sempre acima (Apple-style). Sem floating label.
Success morphvalidação on-blur passouÍcone ✓ aparece à direita com fade + scale 0.8→1250ms ease in
Error shake (sutil)submit com erro de validaçãoTranslação ±3px horizontal 2 vezes400ms total (4 × 100ms)
Clear buttonvalue !== ""Botão X aparece com fade-in à direita150ms ease
Mask formatonChangeReformata enquanto digita (ex: 123456 → 123.456.789-00)instant (no input event)
Suggestion popuptypo detectado on-blur (ex: gmail.con)Hint sob input com sugestão clicável250ms ease in

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o input
EnterSubmete o form pai (se único input ou último)
EscLimpa o input (se houver clear button)
Ctrl/Cmd+ASeleciona 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

TokenValorPapel
T.surface#FFFFFFbackground
T.surface2#FAF8F2background quando disabled
T.border#E3DFD2borda default
T.borderInk#000000borda quando focus
T.brassEdgergba(164,124,43,0.30)outline ring no focus
T.carmim#A32E2Eborda + texto quando error
T.verde#2F6B3Cícone quando success
RADIUS.md12borderRadius
TYPE.base13fontSize do conteúdo
TYPE.sm12fontSize do hint/error
MOTION.fast150mstransition de border