Pular para o conteúdo
Campos de formulário

Componentes

Campos de formulário

Campo de uma linha é pílula; multi-linha é canto 16. Todos focam verde com halo de 3px, todos crescem um degrau no dedo, e nenhum deles é montado à mão numa tela nova: o Field já costura rótulo, controle, dica e erro com as ligações de acessibilidade certas.
ui/input.tsx · textarea · select-nativeui/field.tsxui/form-feedback.tsx

Field — o que você usa

Três variações do mesmo invólucro: Field (input), SelectField e TextareaField. Elas existem porque o fio entre rótulo, erro e dica era digitado 3× por campo e podia divergir em silêncio, quebrando o anúncio do erro no leitor de tela.

Esse nome aparece nas mensagens pros seus pacientes.

Multi-linha é canto 16, não pílula.

componentes reais · o erro do telefone some ao primeiro toque no campo
o uso normal — o resto é derivado
<Field
  name="telefone"                 // pesca errors["telefone"] sozinho
  label="Telefone"
  errors={state?.errors}          // o objeto INTEIRO da server action
  hint="Com DDD. É por aqui que o paciente recebe a confirmação."
/>

// id só quando precisa diferir do name (o mesmo form aberto várias vezes
// na página, com useId por instância).
PropTipoPadrãoO que faz
namestringObrigatório. É a chave do FormData e a chave que o campo pesca em errors.
labelReactNodeVira um <Label htmlFor> de verdade — clicar no texto foca o campo.
errorsRecord<string, string[]>O state.errors inteiro da action. O campo escolhe o próprio.
hintReactNodeApoio entre o controle e o erro. Entra no aria-describedby junto com o erro.
afterReactNodeExtra logo após o controle: um <datalist>, um apoio com cor própria.
classNamestringClasse do WRAPPER (ex.: "sm:col-span-2"), não do controle.

Armadilha já pagaErro de campo não é verdade permanente

O erro que a action devolve é resposta a UM envio. Deixado aceso, ele acusa um campo que a pessoa já corrigiu — foi assim que “3533” apareceu marcado como preço inválido, com a mensagem do envio anterior. O Field apaga ao primeiro toque e reacende no submit do formulário. Não refaça isso por tela.

Armadilha já pagaA <form action> do React 19 RESETA os campos ao responder

Inclusive quando a resposta é um erro de validação: a pessoa perde o que digitou. Sintoma vivido: criar um procedimento virou labirinto — digita o nome, salva, ele pede a duração; preenche a duração, salva, ele pede o nome que o primeiro erro já tinha apagado. E uma senha errada em /entrar limpava o e-mail já digitado.

A cura mora em components/keep-values.ts: useKeepValues() fotografa os campos no onSubmit e devolve como defaultValue. Chame keep.clear() no caminho de SUCESSO de todo formulário que continua montado, senão o próximo envio reposta os valores anteriores.

Texto é o caso fácil. Para <select> e para o Checkbox do Radix o React restaura a partir do valor de MONTAGEM — o estado do React guarda a escolha, o DOM volta pra primeira opção, e o próximo envio grava o valor velho. A primeira opção de papéis é administrador: errar o e-mail e reenviar o convite convidava como ADMIN sem ninguém tocar no campo. Para esses vão keepSelected / keepChecked / keepAll, e a cura fica no CHAMADOR, nunca dentro do wrapper.

Casos com anatomia própria — upload de imagem, conjunto de checkboxes, contador de caracteres — montam as três peças à mão. O Field é pro caso comum, que é a esmagadora maioria.

Input

A pílula de 36px que veste quase tudo.
normal · com valor · inválido · desabilitado · dentro de toolbar (h-8)
DetalheValorPor quê
Alturah-9 · pointer-coarse:h-1036px no mouse, 40px no dedo.
Formarounded-fullCampo de uma linha é coisa que se toca.
Paddingpx-3.5A curva da pílula come espaço: menos que isso encosta o texto na borda.
Tamanho do textotext-base md:text-sm16px no celular de propósito: abaixo disso o Safari dá zoom ao focar o campo.
Fundobg-surfaceNunca transparente — o campo precisa se separar do card.
SombranenhumaBorda 1px + halo de foco fazem o trabalho.
Seleçãoselection:bg-primary selection:text-primary-foregroundO texto selecionado veste a marca.
Autofillbox-shadow interno + text-fill-colorO amarelo do Safari ignora background-color e acenderia no meio de um formulário escuro.

Textarea, Select e Busca

Mesmos tokens, três formas diferentes pela mesma razão: o que o controle faz decide a forma.
Textarea — rounded-xl: pílula cortaria a primeira linha
SelectNative — a pele do Input, a seta do navegador
SearchInput — a lupa é decorativa (pointer-events-none) e o pl-9 abre espaço pra ela

valor: (vazio)

InputOTP — slot é item interno, dígito em mono tabular, cursor falso piscando
ComponenteNota
SelectNativeSelect do navegador, de propósito: no celular ele abre a roleta nativa. Dropdown custom só quando a UX pedir busca ou seleção múltipla — discrição > drama. E o color-scheme por tema é o que faz a lista aberta sair escura num tema escuro.
SearchInputclassName é do INPUT; a LARGURA vai em wrapperClassName (flex-1, sm:max-w-xs). São dois alvos de estilo, então são dois nomes.
InputOTPw-8 até 420px de viewport e w-10 acima: 6 slots + gaps cabem nos ~238px úteis de um card em tela de 320px. Colar limpa não-dígitos (sem isso, um espaço copiado do e-mail descartava o código inteiro em silêncio).
TextareaCuidado com o \r: o transporte multipart normaliza toda quebra de linha pra \r\n. Quem depende de \n precisa normalizar no schema E no consumidor.

Checkbox

A única exceção ao vocabulário de forma: 6px de raio. Redondo pareceria radio — opção única em vez de escolha independente.
Radix Checkbox · marcado veste a marca · size-4 → size-5 no dedo

Armadilha já pagaO reset do React 19 não poupa o checkbox

Quando uma <form action> retorna, o React reseta os controles — e o Radix volta ao valor de MONTAGEM e ainda dispara onCheckedChange(false). A cura mora no CHAMADOR (keepChecked/keepAll em components/keep-values.ts), nunca dentro do wrapper: curar no ui/checkbox.tsx foi aplicado e revertido no mesmo dia, porque a restauração passa por um toggle intermediário e callbacks não-idempotentes apagavam dados.

Erro e apoio

Três peças, três papéis. Nenhuma delas alarma.
PeçaO que éMarcação
FieldErrorO primeiro erro de um campo, em 12px destructive.id={`${inputId}-error`} role="alert"
FormAlertO erro geral do formulário (state.message). Painel discreto em negative-soft — e ele rola até ficar visível quando surge.role="alert" · scroll-mt-20
PositiveBannerA contraparte: estado bom, em positive-soft com escudo.rounded-md px-3 py-2.5
hintInstrução do campo, 12px fg-subtle.id={`${inputId}-hint`}, dentro do aria-describedby

Sim

Telefone

Número inválido. Use DDD + 9 dígitos.

A mensagem diz o que fazer. O campo marca aria-invalid, e o texto está ligado a ele por aria-describedby.

Não

Telefone

Erro de validação: campo inválido (code 422).

Mensagem que descreve o sistema, não o problema. Quem lê não sabe o que corrigir — e o código HTTP não é assunto de quem marca consulta.

FormState.message significa erro: o FormAlert pinta de vermelho. Nunca devolva mensagem no caminho de sucesso — a copy de sucesso é da tela (um toast, um “Salvo ✓” na barra).

Layout de formulário

A grade, o ritmo e o que fica junto.
// Grade: uma coluna no celular, duas a partir de sm.
<div className="grid gap-6 sm:grid-cols-2">
  <Field name="nome" label="Nome" />
  <Field name="telefone" label="Telefone" />
  <TextareaField name="obs" label="Observação" className="sm:col-span-2" />
</div>

// Dentro do Field: space-y-2 entre rótulo, controle, dica e erro.
// Entre blocos de um formulário longo: space-y-phi-3.

O que anda junto

SituaçãoRegra
Campos que se completam (CEP, rua, número)Mesma linha quando couber; o CEP nunca sozinho numa linha inteira.
Campo largo (observação, endereço)sm:col-span-2 — não espremer texto longo em meia largura.
Ações do formulárioRodapé do bloco: Salvar (primária) + Cancelar (ghost). Em tela com barra, o Salvar SOBE pra barra sozinho.
Formulário sempre abertodata-dim-solto: a elevação do foco entra por pseudo-elemento, sem tapete permanente.
Formulário que abre numa linhaGaveta + EditSurface: a linha VIRA a ficha.