Ir ao conteúdo
SEED engenhariaDesign System

Componentes

2. Form-field

estávelseed-componentes.md v1.43 · §02seção 4 de 9802-form-field-campo-de-formulario-estavel-validado.md · MD5 e992bac7

Título completo no canon: Form-field (campo de formulário) — estável · validado pelo Rafael em 2026-08-02 (v0.6)

Emenda v0.7 (2026-08-02, aprovada pelo Rafael — supersedes formais): (a) estados passam de 8 para 9 com a entrada do warning (§2.4); (b) tokens de warning adicionados ao §2.5 com contraste medido; (c) contrato HTML ganha os atributos de teclado virtual (§2.6); (d) fonte do campo em toque = 16px (tokens v1.2). O que estas emendas substituem: "8 estados" onde citado → 9; tabela §2.6 anterior → tabela com as colunas mobile.

O que é este item: o componente-BASE anatômico de todo o Bloco 2. Define a estrutura que envolve QUALQUER controle de entrada — rótulo, texto auxiliar, mensagens, contador — mais estados, tokens, contrato HTML, arquitetura de máscaras, microcopy e acessibilidade. Os 12 itens seguintes do bloco (input de texto, busca, número, textarea, select, combobox, checkbox*, radio*, switch*, slider, date picker, upload) HERDAM esta spec e documentam apenas o que diverge. (*grupos de escolha usam fieldset/legend no lugar de label — divergência documentada nos próprios itens.)

Base de evidência: 4 rodadas de pesquisa (2026-08-02, ~30 fontes primárias cruzadas): Carbon, GOV.UK + família, Material M1–M3, Polaris, Atlassian, Spectrum, USWDS, W3C WAI/WCAG 2.2, NN/g, Baymard, SAP Fiori, gov.br DS, web.dev (Google), Uber Base Web, Stripe, fintechs (Nubank/Revolut/Monzo/Wise), Konjević/Smashing, ecossistema BR de máscaras. Decisões B1–B6 aprovadas pelo Rafael em 2026-08-02 após sobreviverem às 4 rodadas.

2.1 Papel#

Estruturar a comunicação entre o sistema e a pessoa em QUALQUER entrada de dado: o que o sistema pede (rótulo), como preencher (ajuda), o que aconteceu (erro/sucesso) e quanto cabe (contador). O form-field não é um input — é a moldura que faz qualquer input ser compreensível, acessível e consistente em todos os produtos SEED (ERP, chat, site institucional, formulários de captação).

2.2 Anatomia — 5 slots#

 (A) ┌ Rótulo ──────────────── (opcional) ┐   ← label + indicador de opcionalidade
 (B) │ Texto auxiliar pré-digitação*      │   ← só quando PRECISA ser lido antes (exceção B1)
 (C) ┌────────────────────────────────────┐
     │  [controle de entrada]             │   ← input/select/textarea/etc (slot genérico)
     └────────────────────────────────────┘
 (D) │ ⓘ Texto auxiliar  ou  ⚠ Erro       │   ← zona de mensagem (erro SUBSTITUI ajuda — B2)
 (E) │                          120/500   │   ← contador (só quando há limite)
Slot Elemento Regras
A Rótulo (<label for>) Sempre visível, sempre no topo (nunca placeholder como rótulo; nunca flutuante; nunca à esquerda). Montserrat SemiBold 14px, --seed-field-label (13.86:1). Sufixo (opcional) em Regular, --seed-field-optional-text. Variante label-hidden (visually-hidden, permanece p/ leitor de tela) só quando o contexto torna o rótulo redundante — ex.: campo de busca com ícone e placeholder "Buscar…" — regra Polaris: usar com muito critério
B Ajuda pré-digitação (exceção) Entre rótulo e controle SOMENTE quando a instrução precisa ser lida ANTES de digitar (formato obrigatório complexo, decisão pré-preenchimento). Padrão é (D)
C Controle Slot genérico. Altura md 44px (alvo de toque WCAG, igual botão), sm 32px (densidade desktop), lg 52px. Radius --seed-radius-md. Borda 1px --seed-field-border (4.74:1 — é ela que identifica o controle, WCAG 1.4.11). Placeholder: DESENCORAJADO; quando usado, só exemplo de formato prefixado "Ex.:" — nunca instrução, nunca rótulo
D Zona de mensagem Ajuda: --seed-field-help-text (6.55:1 — acima do piso AA, na direção do 7:1 que o GOV.UK adotou após pesquisa). Erro: ícone ⚠ + texto --seed-field-error-text (7.18:1), prefixo "Erro:" visually-hidden p/ leitores de tela. Erro substitui a ajuda (B2); a mensagem de erro DEVE conter a instrução completa de correção (regra editorial que fecha o buraco da substituição). Sem reserva de altura: shift de 1 linha aceito (M3: substituição existe justamente p/ minimizar bump de layout)
E Contador Só quando há limite real. JetBrains Mono 12px (dado de medição = mono, regra dos tokens v1.1). Formato usados/limite. SUPERSEDE (v0.10): a política completa do contador vive no §6.3 (padrão GOV.UK) — o campo NUNCA trunca com maxlength duro; exceder o limite vira estado de ERRO com a contagem do excesso. A versão v0.6–v0.9 desta linha ('ao exceder, vira mensagem de erro' com maxlength no exemplo) fica substituída: o maxlength duro truncava colagem silenciosamente — perda de dado pega na pesquisa do textarea

Espaçamento vertical (mapeado da referência 8/16dp do Material para a escala SEED): rótulo→controle sp-2 (8px) · controle→mensagem sp-2 (8px) · campo→campo sp-6 (24px) confortável / sp-4 (16px) denso (ERP).

2.3 As 6 decisões estruturais (aprovadas 2026-08-02)#

# Decisão Porquê (placar de fontes) Descartado
B1 Ajuda ABAIXO do controle; exceção: instrução pré-digitação vai acima 6×1 (Carbon, Material, Polaris, Spectrum, Atlassian, MUI vs GOV.UK); padrão da stack shadcn; label topo lido 28% mais rápido que lateral (Baymard) Hint sempre acima (GOV.UK — contexto one-thing-per-page de governo, não ERP denso)
B2 Erro SUBSTITUI a ajuda + regra editorial (erro contém a correção completa) 5×1 (Carbon, M1/M3, Atlassian, Spectrum, uxpatterns vs GOV.UK); M3: substituir evita empurrar o layout Empilhar ajuda+erro (dobra a altura no pior momento); reservar altura fixa (desperdiça vertical em ERP)
B3 Produto (ERP/chat): marcar SÓ (opcional). Formulários públicos de conversão: marcar AMBOS (* + (opcional)) Produto: Polaris (proíbe asterisco), Carbon, GOV.UK, Uber ("marque só os menos comuns"). Público: Baymard mediu 32% de falha marcando só opcionais no e-commerce; só 14% marcam ambos Regra única para os dois contextos (a indústria não tem consenso único — tem consenso POR contexto); asterisco no ERP (ruído: quase tudo é obrigatório)
B4 Validação onBlur com reward-early/punish-late + error summary no submit Formulação operacional Polaris: valida quando foco sai E há ≥1 caractere; campo em erro revalida a cada tecla; obrigatório intocado só marca no submit. Refinamento Fiori p/ ERP: entrada massiva agrega erros em registro form-level. NN/g: correção imediata com o campo fresco na memória Validar só no submit (GOV.UK — governo); validar enquanto digita desde a 1ª tecla (validação prematura, irrita — Baymard)
B5 Arquitetura de máscara: REGISTRY EXTENSÍVEL, valor canônico sem formatação, telefone em E.164 Stripe: tolerante na entrada, estrito no armazenamento; Uber: seletor de país + número (2 inputs, menos ambíguo); libphonenumber/E.164 = padrão da indústria; Baymard: 64% não usam máscara localizada e deviam. Crítica do Rafael derrubou o enum fixo: 0800 e clientes EUA não cabiam Enum fechado de máscaras BR (proposta da rodada 3 — fecharia a porta p/ 0800, EUA e máscaras futuras)
B6 Estado success EXISTE, com parcimônia: só onde a confirmação carrega informação real Atlassian/Spectrum/Stripe têm; Stripe marca válido e mede ganho de confiança; Uber alerta contra marcar prematuramente Sem success (perde o ganho em campos críticos: e-mail, documento, disponibilidade); success universal (ruído verde decorativo — GOV.UK desaconselha com razão)

2.4 Estados — 9 (os 6 obrigatórios do §0 + readonly, success e warning¹)#

¹ warning adicionado na emenda v0.7 (era 8 na v0.6 — supersede formal).

Estado Sinais (light) Regras
default borda --seed-field-border (4.74:1)
hover borda --seed-field-border-hover transição 100ms produtiva
focus --seed-focus-ring (anel, NÃO engrossar borda) mecanismo de foco ≠ mecanismo de erro (GOV.UK removeu borda grossa no erro por confundir com foco)
filled valor em --seed-field-value (13.86:1) valor ≠ placeholder tem que ser óbvio (por isso placeholder é 4.74 e valor 13.86)
error borda --seed-field-border-error (5.19:1) + ícone ⚠ + mensagem (7.18:1) + aria-invalid NUNCA só cor (WCAG 1.4.1): são 3 sinais. Erro nunca aparece antes de interação real (B4)
success borda --seed-field-border-success (4.60:1) + ícone ✓ + mensagem opcional só onde confirma algo real (B6)
warning (v0.7) borda --seed-field-border-warning (4.82:1) + ícone ▲ + mensagem (6.68:1) Guarda-corpo duro: SÓ para plausibilidade — dado válido em formato, mas incomum ("consumo 10× acima da sua média — confira"). NUNCA para obrigatoriedade, NUNCA bloqueia submit. Sem o guarda-corpo, warning banaliza e treina a ignorar avisos (por isso o GOV.UK nem o tem). Origem: SAP Fiori/Carbon (ERP)
disabled fundo surface-sunken + texto text-disabled + cursor:not-allowed USO RESTRITO (USWDS: contraste baixo, invisível p/ teclado e leitor de tela) — preferir campo habilitado com validação, ou readonly. Nunca esconder informação necessária num campo disabled
readonly fundo surface-sunken, SEM borda de controle, texto pleno (12.75:1), focável, copiável estado de primeira classe (Fiori/USWDS/Polaris): exibe dado não-editável NESTE contexto (ex.: UC vinda do cadastro). Readonly ≠ disabled: readonly participa do fluxo (tab, cópia, leitor de tela); não recebe estados de validação (Fiori)

loading (9º, condicional): campos com operação assíncrona (CEP consultando ViaCEP, combobox buscando) mostram spinner 16px no slot direito do controle + aria-busy — herda a regra do botão (sem layout shift).

2.5 Tokens de componente (camada 3) — par light/dark obrigatório, contrastes medidos programaticamente (2026-08-02)#

/* Estrutura */
--seed-field-height-sm: 32px;  --seed-field-height-md: 44px;  --seed-field-height-lg: 52px;
--seed-field-radius: var(--seed-radius-md);
--seed-field-gap: var(--seed-sp-2);          /* rótulo→controle e controle→mensagem */
--seed-field-stack-gap: var(--seed-sp-6);    /* campo→campo; sp-4 no denso */

/* Cor — light (medições sobre branco; campo assenta em superfície branca) */
--seed-field-bg: var(--seed-surface-page);               /* branco */
--seed-field-label: var(--seed-cinza-900);               /* 13.86:1 */
--seed-field-value: var(--seed-cinza-900);               /* 13.86:1 */
--seed-field-placeholder: var(--seed-cinza-600);         /* 4.74:1 — AA e distinto do valor */
--seed-field-optional-text: var(--seed-cinza-600);       /* 4.74:1 */
--seed-field-help-text: var(--seed-cinza-700);           /* 6.55:1 — direção GOV.UK 7:1 */
--seed-field-border: var(--seed-cinza-600);              /* 4.74:1 ≥3:1 (WCAG 1.4.11) */
--seed-field-border-hover: var(--seed-cinza-700);
--seed-field-error-text: var(--seed-vermelho-700);       /* 7.18:1 */
--seed-field-border-error: var(--seed-vermelho-600);     /* 5.19:1 */
--seed-field-success-text: var(--seed-turquesa-700);     /* 6.32:1 */
--seed-field-border-success: var(--seed-turquesa-600);   /* 4.60:1 */
--seed-field-readonly-bg: var(--seed-surface-sunken);    /* texto 12.75:1 sobre cinza-50 */
--seed-field-warning-text: var(--seed-dourado-700);      /* 6.68:1 (v0.7) */
--seed-field-border-warning: var(--seed-dourado-600);    /* 4.82:1 (v0.7) */

/* Par dark — REGRA v0.5: token que referencia primitivo DECLARA o par (primitivo não troca de modo).
   Medições: rótulo/mensagens sobre dark-default #141D23; conteúdo do campo sobre dark-raised #1D272D */
[data-theme="dark"] {
  --seed-field-bg: var(--seed-surface-raised);           /* #1D272D */
  --seed-field-label: var(--seed-cinza-100);             /* 14.16:1 */
  --seed-field-value: var(--seed-cinza-100);             /* 12.61:1 */
  --seed-field-placeholder: var(--seed-cinza-400);       /* 6.01:1 */
  --seed-field-optional-text: var(--seed-cinza-400);
  --seed-field-help-text: var(--seed-cinza-300);         /* 8.92:1 */
  --seed-field-border: var(--seed-cinza-500);            /* 4.50:1 (600 sumia; 400 gritava em massa de campos do ERP) */
  --seed-field-border-hover: var(--seed-cinza-400);      /* 6.01:1 */
  --seed-field-error-text: var(--seed-vermelho-300);     /* 8.49:1 */
  --seed-field-border-error: var(--seed-vermelho-400);   /* 5.52:1 */
  --seed-field-success-text: var(--seed-turquesa-300);   /* 9.32:1 */
  --seed-field-border-success: var(--seed-turquesa-300); /* 8.31:1 */
  --seed-field-readonly-bg: var(--seed-surface-sunken);  /* texto = field-value cinza-100 #E3EBF0 sobre #141D23: 14.16:1 (v0.13 — supersede do comentário 9.72:1, que media cinza-300 sobre a página, par que o readonly não renderiza) */
  --seed-field-warning-text: var(--seed-dourado-300);    /* 9.22:1 (v0.7) */
  --seed-field-border-warning: var(--seed-dourado-300);  /* 8.21:1 (v0.7) */
}

2.6 Contrato HTML do campo (a camada que os DS visuais não cobrem)#

Todo campo declara o trio type + inputmode + autocomplete + name/id ESTÁVEIS. Serve três frentes de uma vez: teclado mobile correto, autofill do navegador (conversão) e legibilidade por máquinas — agentes de IA e crawlers entendem o formulário pelos atributos semânticos (a frente "IA/SEO" do site institucional).

Dado type inputmode autocomplete enterkeyhint² autocapitalize/autocorrect/spellcheck² Nota
Nome text name next on / off / off
E-mail email email email next off / off / off Android "corrige" e-mail p/ palavra
Telefone tel tel tel next off / off / off armazena E.164 (§2.7)
CPF/CNPJ/CEP/UC text numeric — (cpf não tem token oficial; cep usa postal-code) next off / off / off NUNCA type=number (só p/ quantidade incremental — web.dev); number quebra máscara, zeros à esquerda e leitores
Endereço text street-address / address-line1… next on / on / on
Valores (R$, kWh) text decimal next off / off / off JetBrains Mono no valor (dado de medição)
Senha password current-password / new-password done off / off / off norma §3.5

² Colunas adicionadas na emenda v0.7 (mobile): enterkeyhint rotula a tecla Enter do teclado virtual ("próximo" no meio do fluxo, "concluído"/done no último campo, search na busca); o trio autocapitalize/autocorrect/spellcheck desligado em identificadores impede o Android de "corrigir" UC/códigos para palavras. SC 3.3.7 Redundant Entry (WCAG 2.2): dado já fornecido no mesmo fluxo não pode ser exigido de novo por transcrição — auto-popular ou oferecer seleção (mata o "confirme seu e-mail").

Regras: name/id nunca gerados aleatoriamente por render (autofill exige estabilidade — web.dev) · formulário real <form> com botão de submit · botão de submit NUNCA desabilitado por campos vazios (Atlassian): a validação e o error summary dizem o que falta.

2.6b Grupos de controles — fieldset + legend (emenda v0.12)#

Quando o "campo" é um GRUPO (radios §9, checkboxes §10, grupos de switch §8), o slot A vira <legend> dentro de <fieldset>: a pergunta é a legend; ajuda e erro do form-field continuam valendo via aria-describedby. Regra declarada UMA vez aqui — as specs da família citam. (GOV.UK/NHS/USWDS unânimes; a legend é lida junto de cada opção pelos leitores, dando contexto sem repetição.)

2.7 Máscaras — registry extensível (decisão B5)#

Três princípios arquiteturais:

  1. Máscara é apresentação. O valor canônico armazenado NÃO tem formatação: CPF = 11 dígitos, CEP = 8, telefone = E.164 (+5533999999999, +14155552671). A exibição formata; o dado viaja limpo (bancos, APIs, SMS esperam E.164 — padrão libphonenumber/Google).
  2. Tolerante na entrada, estrita no armazenamento (Stripe). Colar 033 99999-9999, (33)999999999 ou 33999999999 → tudo aceito e normalizado. Nunca rejeitar por pontuação.
  3. Registry aberto. Máscaras são registráveis por nome (registerMask("placa-mercosul", …)) — produto novo adiciona máscara sem alterar o componente. NÃO é enum fechado (alternativa descartada na rodada 3 pela crítica do Rafael: 0800 e clientes EUA não cabiam).

Registry inicial:

Máscara Exibição Canônico Notas
cpf 000.000.000-00 11 díg. valida dígitos verificadores
cnpj 00.000.000/0000-00 14 díg. idem
cpf-cnpj dinâmica pelo comprimento 11 ou 14 campo único B2C+B2B (padrão BR)
cep 00000-000 8 díg. + consulta ViaCEP (loading no campo; falha da API NUNCA bloqueia — preenchimento manual sempre possível)
tel-br (00) 0000-0000 / (00) 9 0000-0000 E.164 +55… detecta fixo/celular pelo comprimento
tel-nao-geografico 0800 000 0000 dígitos puros 0800/0300/0500/4004 — sem DDD, sem E.164 de assinante, categoria própria
tel-internacional seletor de país + número nacional E.164 padrão Uber (2 inputs); default Brasil; cobre EUA e qualquer país; trocar país MANTÉM o número digitado (Evil Martians); validação libphonenumber
moeda R$ 1.234,56 (ou US$ por locale) decimal
data DD/MM/AAAA ISO 8601 tolera colar 01012026

Armadilhas de implementação (documentadas pela comunidade BR — bugs reais a evitar): maxLength conta os caracteres DA MÁSCARA (CPF = 14, não 11) · reposicionar o cursor após formatação programática (o value re-setado joga o cursor pro fim) · autofill do navegador entrega valor sem máscara → normalizar no change, não só no keypress.

2.8 Microcopy — tom SEED (vocabulário PT-BR alinhado ao gov.br DS: rótulo, texto auxiliar, mensagem)#

  1. Rótulo: substantivo curto, sentence case, sem dois-pontos. "Unidade consumidora", "E-mail", "CNPJ". Nunca instrução no rótulo.
  2. Texto auxiliar: 1 linha, diz FORMATO ou PORQUÊ. "Somente números." · "Está na sua fatura de energia, no canto superior." Nunca indica obrigatoriedade (regra Uber: esse espaço é de formato/erro).
  3. Erro = o que houve + como corrigir, na voz SEED (direto, acolhedor, sem culpar): ✅ "CPF incompleto — digite os 11 números." · "CEP não encontrado — confira ou preencha o endereço manualmente." | ❌ "Erro no campo." · "Entrada inválida." · "Você digitou errado." Específico > genérico (Baymard: "CEP curto demais" > "Inválido").
  4. Dado pessoal explica o porquê (LGPD + prática fintech): texto auxiliar do CPF: "Usamos seu CPF apenas para emitir a proposta." Coleta sem porquê visível é atrito e risco.
  5. (opcional) literal em minúsculas no produto; formulário público de conversão usa * + legenda "Campos com * são obrigatórios" no topo (B3).

2.9 Acessibilidade#

<label for> sempre (nunca div-rotulada) · aria-describedby na ordem erro ANTES da ajuda ("err-id help-id" — AUI: o erro é anunciado primeiro) · aria-invalid só após validação real, NUNCA no estado inicial (W3C ARIA21), e SEMPRE com valor explícito "true" — nunca toggleAttribute, que grava o atributo com valor vazio: [aria-invalid="true"] do CSS não casa e a borda de erro some (bug flagrado na validação executada de 2026-08-03) · mensagem de erro com prefixo "Erro:" visually-hidden · erro dinâmico anunciado (região aria-live="assertive" ou role="alert") · ícones de estado com par textual (1.4.1) · foco: anel, nunca borda engrossada · contraste: TODOS os pares da §2.5 medidos ≥4.5:1 texto e ≥3:1 não-texto · zoom 400%: layout de coluna única resiste · toque ≥44px no md.

2.10 Recuperação de erro no formulário (nível form — regras que o form-field expõe)#

  1. NUNCA limpar campos após erro (Baymard/Stripe: re-digitar é gatilho de abandono nº1).
  2. Error summary no submit para formulário com 3+ campos: bloco no topo com título instrucional + lista de links-âncora para cada erro; foco move pro summary (GOV.UK); autoscroll até o primeiro campo em erro (Baymard).
  3. Erro também no <title> da página quando o submit recarrega (USWDS — leitores de tela anunciam antes de tudo).
  4. ERP/entrada massiva (Fiori): validação por campo no blur (B4) + agregado de erros form-level com contador; teclado flui sem interrupção. Detalhe: ponte com o Bloco 3 (alert/banner) — o componente de summary nasce lá.

2.11 Código — HTML/CSS#

<div class="seed-field">
  <label class="seed-field__label" for="cpf">
    CPF <span class="seed-field__optional">(opcional)</span>
  </label>
  <input class="seed-field__control" id="cpf" name="cpf" type="text"
         inputmode="numeric" maxlength="14" placeholder="Ex.: 000.000.000-00"
         aria-describedby="cpf-err cpf-help">
  <p class="seed-field__help" id="cpf-help">Usamos seu CPF apenas para emitir a proposta.</p>
  <p class="seed-field__error" id="cpf-err" hidden>
    <span class="seed-visually-hidden">Erro:</span>
    <svg class="seed-field__error-icon" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm-.9 3.5h1.8v5h-1.8v-5Zm.9 8.4a1.1 1.1 0 1 1 0-2.2 1.1 1.1 0 0 1 0 2.2Z"/></svg>
    CPF incompleto — digite os 11 números.
  </p>
</div>
[hidden]{display:none!important} /* ARMADILHA (bug real pego pelo Rafael na validação do preview v0.7, 2026-08-02): classes de mensagem usam display:flex, que vence o display:none que o navegador aplica ao atributo `hidden` — sem esta regra, erro e sucesso aparecem SEMPRE. No React o problema não existe (renderização condicional), mas todo consumo HTML/CSS puro precisa desta linha. */
.seed-field{display:flex;flex-direction:column;gap:var(--seed-field-gap)}
.seed-field__label{font-family:var(--seed-font-sans);font-weight:var(--seed-fw-semibold);
  font-size:14px;color:var(--seed-field-label)}
.seed-field__optional{font-weight:400;color:var(--seed-field-optional-text)}
.seed-field__control{height:var(--seed-field-height-md);padding:0 12px;
  font-family:var(--seed-font-sans);font-size:14px;color:var(--seed-field-value);
  background:var(--seed-field-bg);border:1px solid var(--seed-field-border);
  border-radius:var(--seed-field-radius);
  transition:border-color var(--seed-dur-productive-fast) var(--seed-ease-productive)}
.seed-field__control::placeholder{color:var(--seed-field-placeholder)}
.seed-field__control:hover:not(:disabled):not([readonly]){border-color:var(--seed-field-border-hover)}
.seed-field__control:focus-visible{outline:none;box-shadow:var(--seed-focus-ring)}
.seed-field__control[aria-invalid="true"]{border-color:var(--seed-field-border-error)}
.seed-field__control[data-valid="true"]{border-color:var(--seed-field-border-success)}
.seed-field__control:disabled{background:var(--seed-surface-sunken);
  color:var(--seed-text-disabled);cursor:not-allowed}
.seed-field__control[readonly]{background:var(--seed-field-readonly-bg);border-color:transparent}
.seed-field__help{font-size:12px;color:var(--seed-field-help-text);margin:0}
.seed-field__error{display:flex;align-items:center;gap:6px;font-size:12px;
  font-weight:var(--seed-fw-semibold);color:var(--seed-field-error-text);margin:0}
.seed-field__counter{font-family:var(--seed-font-mono);font-size:12px;
  color:var(--seed-field-help-text);align-self:flex-end}
.seed-visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;
  padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

2.12 Código — React/TypeScript (padrão shadcn de composição; RHF pluga por cima)#

import * as React from "react";
import { cn } from "@/lib/utils";
import { CircleAlert, CircleCheck } from "lucide-react";

type FieldCtx = { id: string; helpId: string; errId: string;
  invalid?: boolean; valid?: boolean };
const FieldContext = React.createContext<FieldCtx | null>(null);
const useField = () => {
  const ctx = React.useContext(FieldContext);
  if (!ctx) throw new Error("Componentes Field* devem viver dentro de <FormField>");
  return ctx;
};

export function FormField({ id, invalid, valid, className, children }:
  React.PropsWithChildren<{ id: string; invalid?: boolean; valid?: boolean; className?: string }>) {
  const ctx = { id, helpId: `${id}-help`, errId: `${id}-err`, invalid, valid };
  return (
    <FieldContext.Provider value={ctx}>
      <div className={cn("flex flex-col gap-2", className)}>{children}</div>
    </FieldContext.Provider>
  );
}

export function FieldLabel({ optional, children }:
  React.PropsWithChildren<{ optional?: boolean }>) {
  const { id } = useField();
  return (
    <label htmlFor={id} className="text-sm font-semibold text-[var(--seed-field-label)]">
      {children}{" "}
      {optional && <span className="font-normal text-[var(--seed-field-optional-text)]">(opcional)</span>}
    </label>
  );
}

export function FieldControl(props: React.InputHTMLAttributes<HTMLInputElement>) {
  const { id, helpId, errId, invalid, valid } = useField();
  return (
    <input
      id={id}
      aria-invalid={invalid || undefined}
      aria-describedby={cn(invalid && errId, helpId) || undefined} /* erro ANTES da ajuda */
      data-valid={valid || undefined}
      className={cn(
        "h-11 rounded-md border bg-[var(--seed-field-bg)] px-3 text-sm",
        "text-[var(--seed-field-value)] placeholder:text-[var(--seed-field-placeholder)]",
        "border-[var(--seed-field-border)] hover:border-[var(--seed-field-border-hover)]",
        "transition-colors duration-100 focus-visible:outline-none",
        "focus-visible:ring-2 focus-visible:ring-[var(--seed-border-focus)] focus-visible:ring-offset-2",
        "disabled:cursor-not-allowed disabled:bg-[var(--seed-surface-sunken)] disabled:text-[var(--seed-text-disabled)]",
        "read-only:border-transparent read-only:bg-[var(--seed-field-readonly-bg)]",
        invalid && "border-[var(--seed-field-border-error)]",
        valid && "border-[var(--seed-field-border-success)]",
      )}
      {...props}
    />
  );
}

export function FieldHelp({ children }: React.PropsWithChildren) {
  const { helpId, invalid } = useField();
  if (invalid) return null; /* B2: erro substitui a ajuda */
  return <p id={helpId} className="text-xs text-[var(--seed-field-help-text)]">{children}</p>;
}

export function FieldError({ children }: React.PropsWithChildren) {
  const { errId, invalid } = useField();
  if (!invalid) return null;
  return (
    <p id={errId} role="alert"
       className="flex items-center gap-1.5 text-xs font-semibold text-[var(--seed-field-error-text)]">
      <span className="sr-only">Erro:</span>
      <CircleAlert className="size-4 shrink-0" aria-hidden />
      {children}
    </p>
  );
}

export function FieldSuccess({ children }: React.PropsWithChildren) {
  const { valid } = useField();
  if (!valid) return null;
  return (
    <p className="flex items-center gap-1.5 text-xs font-semibold text-[var(--seed-field-success-text)]">
      <CircleCheck className="size-4 shrink-0" aria-hidden />
      {children}
    </p>
  );
}

Hook de validação B4 (reward-early/punish-late) e utilitários de máscara (registry + libphonenumber-js) são entregues no item "input de texto" deste bloco, que é o primeiro consumidor concreto.

2.13 Decisões deste item (além de B1–B6, com alternativas descartadas)#

Decisão Porquê Descartado
Form-field como componente-base do bloco 13 itens compartilham a mesma moldura; especificar 1 vez evita 13 variações Especificar label/ajuda/erro dentro de cada item (deriva incoerência)
Ajuda em cinza-700 (6.55:1), não cinza-600 GOV.UK escureceu o hint p/ 7:1 após usuários não lerem; 700 é o stop SEED mais próximo dessa direção mantendo hierarquia c/ o valor cinza-600 (4.74 — passa AA mas repete o erro que o GOV.UK corrigiu)
Erro texto em vermelho-700 (7.18:1), borda em vermelho-600 Texto de 12px pede folga acima do piso; borda só precisa 3:1 e o 600 casa com o botão destructive Tudo em vermelho-500 (4.30:1 medido — serve p/ borda ≥3, mas REPROVA texto normal <4.5)
Borda dark = cinza-500 (4.50:1) cinza-600 somia no dark; cinza-400 em massa de campos do ERP grita (outline de botão usa 400 porque botão é ação — campo é massa) Reusar o token do botão outline (papéis diferentes: ação pontual vs grade de campos)
Foco = anel; erro = cor de borda + ícone + texto Mecanismos distintos por estado (GOV.UK removeu borda grossa do erro por colidir com foco) Engrossar borda no erro (colisão de sinal)
Contador em JetBrains Mono "usados/limite" é dado de medição — papel exclusivo da mono nos tokens v1.1 Montserrat (números dançam sem tabular)
ViaCEP com fallback manual sempre API externa cai; endereço nunca pode ficar refém CEP obrigatório via API (bloqueio real de cadastro)

2.14 Aplicação dos 6 testes (§1.12)#

  1. Container 3:1: borda default 4.74:1 (light) / 4.50:1 (dark); erro 5.19/5.52; success 4.60/8.31 — MEDIDOS, tabela §2.5. ✅
  2. Escada de mecanismos: estados não diferem só por tom — error = borda+ícone+mensagem; success = borda+ícone; readonly = REMOVE a borda e muda o fundo; disabled = fundo+cursor+texto; focus = anel. ✅
  3. Grayscale: ícones ⚠/✓ e a presença/ausência de borda carregam os estados sem matiz — faixa cinza pré-renderizada no preview. ✅
  4. Teste do par: campo default × campo em erro lado a lado no preview — distinguíveis a 1s de distância de braço. ✅
  5. Regra do um: adaptação p/ formulário — UMA mensagem por campo por vez (B2 + regra Uber de nunca empilhar validações). ✅
  6. Estado atual: campo em erro/sucesso/readonly identificável DE RELANCE por quem chega agora, sem interagir (3 sinais no erro; fundo+ausência de borda no readonly). ✅

BancadaSEED DS v2 — Bloco 2 · Preview v0.13 (13/13)abrir em página própria ↗

Também cita o §2: tela-autenticacao, tela-busca.

Esc