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ãoclose(×), 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 dehelperText) — 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.
Estados
Seis estados. Focused e filled são demonstrados com classes de demonstração estática. Hover interno (cursor de texto) é nativo do browser.
Tokens aplicáveis
| Token | Valor | Papel |
|---|---|---|
--color-surface-carved | #EFEFEF | Fundo do input (estado default) |
--color-surface-base | #F7F7F7 | Fundo do input (estado focused) |
--shadow-carved | inset 0 4px 16px 0 rgba(0,0,0,0.08) | Sombra interna do campo (default) |
--border-carved-top | #E8E8E8 | Borda superior do campo (sombra do sulco) |
--border-carved-bottom | #FFFFFF | Borda inferior do campo (luz do sulco) |
--radius-md | 16px | Raio do campo |
--type-family-sans | Instrument Sans | Família tipográfica |
--type-size-md | 16px | Tamanho do valor digitado |
--type-size-xs | 12px | Tamanho do label |
--type-size-sm | 14px | Tamanho do helper text e error message |
--type-weight-medium | 500 | Peso do label |
--type-weight-regular | 400 | Peso do valor, placeholder, helper, error |
--type-tracking-loose | 0.08em | Letter-spacing do label uppercase |
--color-ink-primary | #171717 | Cor do valor digitado; contorno de foco |
--color-ink-secondary | #6B6B6B | Cor do label e do helper text |
--color-ink-tertiary | #A1A1A1 | Cor do placeholder, labelHint e ícone em repouso |
--color-functional-error | vermelho funcional | Borda do campo no estado error; cor do errorMessage |
--color-functional-info | #3875B0 | Focus ring do iconTrailing (botão close) |
--focus-ring-width | 2px | Espessura do focus ring do botão trailing |
--focus-ring-offset | 2px | Afastamento do focus ring do botão trailing |
--icon-size-sm | 16px | Tamanho do iconTrailing |
--space-8 | 8px | Gap label↔input, input↔helper, gap interno leading |
--space-12 | 12px | Padding vertical interno do campo |
--space-16 | 16px | Padding 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 comhtmlFor. O componente usa<label htmlFor="id">+<input id="id">— associação explícita. Não usearia-labelno 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-describedbyapontando para oiddoerrorMessagequando o campo está em erro. Garante que o leitor de tela anuncie a mensagem.aria-describedbyapontando para oiddohelperTextquando aplicável. V1 trata exclusividade: ou helper ou error — simplifica o gerenciamento dearia-describedby.readonlyedisabled. Atributos HTML nativos no<input>.readonlymantém o campo no fluxo de teclado (consultivo);disabledo 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>comaria-label="Limpar campo", focus ring via:focus-visibleusando--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>