Text Input

Campo de entrada de texto com label visível, helper text, estados de erro e suporte a múltiplos tipos. Irmão do Search Field — mesma superfície cavada, sem ícone leading.

Quando usar

  • Capturas de dado identificado — e-mail, nome, telefone, URL, senha — onde o label visível explica o objeto da entrada.
  • Formulários de onboarding, cadastro ou configuração onde cada campo precisa de contexto explícito.
  • Entradas que podem resultar em erro de formato e precisam de errorMessage descritivo abaixo do campo.
  • Campos que exigem helper text explicativo ("Formato: (DDD) número.", "Usamos pra mandar o magic link.").

Quando não usar

  • Busca textual em coleção — usar Search Field, que já carrega semântica de role="search" e ícone de lupa.
  • Filtragem categórica — usar Chip focável.
  • Entrada de valor monetário ou quantidade financeira — o produto usa campos formatados específicos com máscara, não type="number" cru.
  • Entrada de texto longo (parágrafo, descrição) — usar <textarea> independente (componente v2+).

Anatomia

O Text Input tem 4 partes obrigatórias/opcionais: label (sempre visível), labelHint (opcional inline), o campo cavado com input + iconTrailing opcional, e helperText ou errorMessage abaixo (mutuamente exclusivos na v1).

Partes do componente:

  • label — uppercase xs tracking-loose weight-500, cor --color-ink-secondary. Sempre presente, sempre visível — diferença fundamental do Search Field, que usa só aria-label.
  • labelHint (opcional) — inline após o label, minúsculo entre parênteses: (obrigatório) ou (opcional). Cor --color-ink-tertiary, sem uppercase, sem letter-spacing.
  • input — campo sobre --color-surface-carved. Raio --radius-md (16px). Sem ícone leading — distingue visualmente do Search Field.
  • iconTrailing (opcional) — botão close (×), 16px, stroke 1.5px. Aparece apenas quando o campo tem valor.
  • helperText (opcional) — sm regular --color-ink-secondary. Explica formato ou razão da pergunta.
  • errorMessage (opcional, exclusivo de helperText) — sm regular --color-functional-error. Descreve o que está errado.

Variantes

Cinco variantes por tipo de input. O visual é idêntico — o type HTML governa comportamento nativo (teclado virtual em mobile, validação de formato, mascaramento de senha).

Variantes por canal. app: componente visual (default). voz: colapsa em turno de diálogo — o assistente faz a pergunta ("qual seu e-mail?") e captura a resposta diretamente. whatsapp, e-mail, sms: não-aplicável — o canal é textual/linear, o usuário digita direto no fluxo.

type="text"
type="email"
type="tel"
type="url"
type="password"

Estados

Seis estados. Focused e filled são demonstrados com classes de demonstração estática. Hover interno (cursor de texto) é nativo do browser.

default
focused
filled
disabled
readOnly
error

Tokens aplicáveis

TokenValorPapel
--color-surface-carved#EFEFEFFundo do input (estado default)
--color-surface-base#F7F7F7Fundo do input (estado focused)
--shadow-carvedinset 0 4px 16px 0 rgba(0,0,0,0.08)Sombra interna do campo (default)
--border-carved-top#E8E8E8Borda superior do campo (sombra do sulco)
--border-carved-bottom#FFFFFFBorda inferior do campo (luz do sulco)
--radius-md16pxRaio do campo
--type-family-sansInstrument SansFamília tipográfica
--type-size-md16pxTamanho do valor digitado
--type-size-xs12pxTamanho do label
--type-size-sm14pxTamanho do helper text e error message
--type-weight-medium500Peso do label
--type-weight-regular400Peso do valor, placeholder, helper, error
--type-tracking-loose0.08emLetter-spacing do label uppercase
--color-ink-primary#171717Cor do valor digitado; contorno de foco
--color-ink-secondary#6B6B6BCor do label e do helper text
--color-ink-tertiary#A1A1A1Cor do placeholder, labelHint e ícone em repouso
--color-functional-errorvermelho funcionalBorda do campo no estado error; cor do errorMessage
--color-functional-info#3875B0Focus ring do iconTrailing (botão close)
--focus-ring-width2pxEspessura do focus ring do botão trailing
--focus-ring-offset2pxAfastamento do focus ring do botão trailing
--icon-size-sm16pxTamanho do iconTrailing
--space-88pxGap label↔input, input↔helper, gap interno leading
--space-1212pxPadding vertical interno do campo
--space-1616pxPadding horizontal interno do campo

Conteúdo

  • Label específico, não genérico. "E-MAIL", "NOME", "WHATSAPP" — nunca "CAMPO DE TEXTO" ou "DIGITE AQUI". O objeto da entrada está no label.
  • LabelHint canônico. (obrigatório) ou (opcional) minúsculo entre parênteses — inline ao lado do label, sem uppercase. Só um dos dois; nunca ambos no mesmo campo.
  • Placeholder decorativo. Exemplo do formato esperado: "seu@email.com", "(11) 99999-9999", "https://exemplo.com". Não repete o label. Sem reticências.
  • HelperText conciso. Explica formato ou motivo da pergunta. "Usamos pra mandar o magic link." / "Formato: (DDD) número." Sem instrução condescendente do tipo "Por favor, preencha seu e-mail abaixo".
  • ErrorMessage específico, seco. Diz o que está errado. "E-mail inválido. Confere o @." / "Esse formato não bate." Nunca "Algo deu errado!" ou "Ops!".
  • Sem emoji. O Text Input não usa emoji em nenhuma parte — label, placeholder, helper ou error.

Régua de voz: Voz & Tom §2.1 — abre direto no que importa.

Acessibilidade

  • <label> como wrapper com htmlFor. O componente usa <label htmlFor="id"> + <input id="id"> — associação explícita. Não use aria-label no lugar de label visível; o ponto central do Text Input vs. Search Field é o label visível.
  • aria-required="true" quando o campo é obrigatório (complementa o labelHint "(obrigatório)" com semântica AT).
  • aria-invalid="true" + aria-describedby apontando para o id do errorMessage quando o campo está em erro. Garante que o leitor de tela anuncie a mensagem.
  • aria-describedby apontando para o id do helperText quando aplicável. V1 trata exclusividade: ou helper ou error — simplifica o gerenciamento de aria-describedby.
  • readonly e disabled. Atributos HTML nativos no <input>. readonly mantém o campo no fluxo de teclado (consultivo); disabled o remove.
  • Focus ring do campo. O contorno ink-primary (inset shadow 1px) sinaliza visualmente o campo ativo. Não suprimido em nenhuma condição.
  • Focus ring do iconTrailing. Botão <button> com aria-label="Limpar campo", focus ring via :focus-visible usando --color-functional-info. Navegável por teclado.
  • Role="alert" no errorMessage. Marcado com role="alert" para leitura automática pelo AT quando o estado de erro é ativado.
  • Contraste. --color-ink-primary (#171717) sobre --color-surface-carved (#EFEFEF) passa WCAG AA. Label em --color-ink-secondary (#6B6B6B) também passa AA. Placeholder e labelHint em --color-ink-tertiary (#A1A1A1) são decorativos — não carregam informação única.

Do / Don't

Fazer

Label específico nomeia o objeto da entrada. O hint (obrigatório) acrescenta contexto sem ruído.

Não fazer

"CAMPO" não diz o que capturar. "Digite aqui..." repete o óbvio e acrescenta reticências performáticas.

Fazer

surface-carved + shadow-carved. O campo afunda na superfície — distinção clara de papel em relação ao Card que o contém.

Não fazer

Input com visual raised inverte a hierarquia. Campo de entrada afunda; Card que o contém eleva. Anti-padrão visual — o campo "sobe" acima do container.

Fazer

Erro específico — diz o que está errado e onde corrigir. Seco, sem drama, sem "Ops!".

Não fazer

Erro genérico não diz o que corrigir. O usuário não sabe o que está errado nem como resolver.

Componentes relacionados

  • Search Field — irmão do Text Input. Usa a mesma superfície cavada mas com ícone leading fixo (lupa) e semântica de busca. Use Search Field para filtro textual de coleção; Text Input para captura de dado identificado.
  • Button — a submissão do formulário que contém o Text Input. Sempre um type="submit" separado, nunca colado ao campo.
  • Card — container natural para formulários com Text Input. O card raised eleva o contexto; o input carved afunda dentro dele — hierarquia correta.

Markup de referência

HTML copiável — semente da story de componente. CSS aplicável: componente-text-input.css.

<!-- Text Input — estado default com helperText --> <label className="peppe-text-input" htmlFor="email"> <span className="peppe-text-input__label-text"> E-MAIL <span className="peppe-text-input__label-hint">(obrigatório)</span> </span> <div className="peppe-text-input__input-wrap"> <input className="peppe-text-input__input" id="email" type="email" placeholder="seu@email.com" autoComplete="email" aria-required="true" aria-describedby="email-helper"> </div> <p className="peppe-text-input__helper" id="email-helper"> Usamos pra mandar o magic link. </p> </label> <!-- Text Input — estado filled (com iconTrailing) --> <label className="peppe-text-input" htmlFor="email-filled"> <span className="peppe-text-input__label-text">E-MAIL</span> <div className="peppe-text-input__input-wrap"> <input className="peppe-text-input__input" id="email-filled" type="email" value="thiago@peppe.ai" autoComplete="email"> <!-- iconTrailing: botão, não decoração --> <button className="peppe-text-input__icon-trailing" type="button" aria-label="Limpar campo"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true"> <line x1="4" y1="4" x2="12" y2="12"/> <line x1="12" y1="4" x2="4" y2="12"/> </svg> </button> </div> </label> <!-- Text Input — estado error --> <label className="peppe-text-input peppe-text-input--error" htmlFor="email-error"> <span className="peppe-text-input__label-text">E-MAIL</span> <div className="peppe-text-input__input-wrap"> <input className="peppe-text-input__input" id="email-error" type="email" value="thiago@" autoComplete="email" aria-invalid="true" aria-describedby="email-error-msg"> </div> <p className="peppe-text-input__error" id="email-error-msg" role="alert"> E-mail inválido. Confere o @. </p> </label> <!-- Text Input — estado disabled --> <label className="peppe-text-input peppe-text-input--disabled" htmlFor="email-disabled"> <span className="peppe-text-input__label-text">E-MAIL</span> <div className="peppe-text-input__input-wrap"> <input className="peppe-text-input__input" id="email-disabled" type="email" placeholder="seu@email.com" disabled> </div> </label> <!-- Text Input — estado readOnly --> <label className="peppe-text-input peppe-text-input--readonly" htmlFor="email-readonly"> <span className="peppe-text-input__label-text">E-MAIL</span> <div className="peppe-text-input__input-wrap"> <input className="peppe-text-input__input" id="email-readonly" type="email" value="thiago@peppe.ai" readonly> </div> </label>