---
fonte: 01-canonicos/seed-componentes.md
versao_da_fonte: v1.43
secao: 06
titulo: "Textarea — `estável` · validado pelo Rafael em 2026-08-02 (v0.10)"
sequencia: 8 de 98
bytes_do_corpo: 8510
md5_do_corpo: f29b623b70818e40f2b9d1df88876ddf
gerado_por: 06-validacao/geradores/gen-camada-ia.py
nota: fatia GERADA — o corpo abaixo é byte a byte o trecho do canônico; edite o canônico, nunca esta fatia. Canônico inteiro em https://ds.seed.eng.br/01-canonicos/seed-componentes.md
---
## 6. Textarea — `estável` · validado pelo Rafael em 2026-08-02 (v0.10)

> **Consome o form-field (§2).** Documenta o que é do textarea: auto-grow, a política de limite de caracteres (que SUPERSEDE o slot E do §2.2), os canais separados de contagem, o resize acessível e o comportamento do Enter por contexto.
>
> **Base de evidência (3 rodadas encadeadas, 2026-08-02):** R1 — GOV.UK/NHS character count (o componente com a auditoria de a11y mais profunda do gênero: 4 bugs de leitores documentados e corrigidos + auditoria DAC), Carbon text area, `field-sizing: content` (CSS 2023+, Coyier/Walsh/Willison) e a técnica grid de fallback. R2 (herda "Enter faz o quê?") — Slack/WhatsApp (Enter envia + Shift+Enter quebra, com preferência invertível no Slack), GitLab Duo (fricção real documentada de prompts multilinha), mobile universal (Return quebra, botão envia). R3 (herda "crescer até onde e quem controla?") — WCAG 2.2 SC 2.5.7 Dragging (AA legal: arrasto exige alternativa de ponteiro único; teclado NÃO basta) + issue de a11y do próprio Carbon contra o resize handle. Decisões F1–F6 aprovadas pelo Rafael em 2026-08-02.

### 6.1 Papel e fronteiras

Entrada de texto livre multilinha: observações, descrições, mensagens, justificativas. **Fronteira (F6):** editor rich-text/markdown (negrito, listas, anexos inline) NÃO é textarea — é componente futuro fora do Bloco 2 (o chat SEED evoluirá para ele; o textarea é a fundação). Uma linha só → input (§3).

### 6.2 As 6 decisões (F1–F6, aprovadas 2026-08-02)

| # | Decisão | Porquê | Descartado |
|---|---|---|---|
| F1 | **Auto-grow por padrão**: `field-sizing: content` + `min-height: calc(3lh + 2·padding)` e `max-height: calc(8lh + 2·padding)` com scroll; fallback = técnica grid (::after espelhado) p/ navegadores sem suporte. Chat: mínimo 1lh | O campo acompanha o conteúdo — elimina o conflito altura fixa × texto longo; CSS nativo 2023+ sem JS | Altura fixa com scroll interno (esconde o que a pessoa escreveu); auto-grow ilimitado ("cresce como King Kong" — página inteira vira campo) |
| F2 | **Limite de caracteres SEM truncamento** (padrão GOV.UK): campo aceita ultrapassar; exceder → estado de ERRO com contagem do excesso ("Você ultrapassou o limite em 12 caracteres"); contador aparece a partir do threshold (75% do limite) quando o limite é alto (>100) | GOV.UK/NHS: permite colar a resposta inteira e editar; `maxlength` duro TRUNCA colagem silenciosamente = perda de dado; threshold elimina ruído em limites técnicos altos | maxlength duro (o padrão da web — e a política v0.6 do nosso slot E, superseded); contador sempre visível em limite de 32k (ansiedade inútil) |
| F3 | **Canais separados de contagem**: visual atualiza a cada tecla; leitor de tela ouve na PAUSA da digitação (live region própria com debounce); limite anunciado no `aria-describedby` desde o primeiro foco; contador com id separado do texto auxiliar | Os 4 bugs corrigidos do GOV.UK: anúncio abaixo do threshold, mensagens obsoletas enfileiradas, hint lido junto da contagem; auditoria DAC: auditor cego só soube do limite após 50 caracteres | Um nó só para ver e ouvir (a origem dos 4 bugs); contagem em aria-live assertive (interrompe a própria digitação) |
| F4 | Resize manual: **vertical-only, complemento desktop**; o auto-grow É a alternativa de ponteiro único exigida | WCAG 2.2 SC 2.5.7 (AA, legal): função por arrasto precisa de alternativa sem arrasto — teclado não satisfaz ESTE critério; com auto-grow, arrastar vira conveniência, nunca função exclusiva; Carbon tem issue aberta por não ter alternativa | Resize horizontal (quebra layout — Carbon proíbe); handle como único controle de altura (falha 2.5.7); remover o handle (perde a conveniência sem ganho) |
| F5 | **Enter por contexto**: formulário → Enter quebra linha (nativo), envio SÓ por botão; chat desktop → Enter envia + Shift+Enter quebra, COM preferência invertível (Enter quebra / Ctrl+Enter envia); mobile → Return quebra + botão envia SEMPRE | Padrão universal Slack/WhatsApp + a inversão que o Slack oferece e o GitLab documenta como necessidade real (prompts longos enviados incompletos); no toque não há convenção de envio por tecla | Enter-envia hardcoded (pune quem escreve longo); Enter-envia em formulário (submit acidental no meio da observação) |
| F6 | Fronteira rich-text registrada (§6.1) | Escopo do bloco é entrada de texto plano; editor é outra classe de componente | Markdown "básico" embutido (meio-editor: pior dos dois mundos) |

### 6.3 Contador — anatomia e política (a versão canônica; o slot E do §2.2 aponta para cá)

```
[textarea auto-grow]
Texto auxiliar (id próprio)                     87/400   ← visual: cada tecla; some abaixo do threshold
[região aria-live=polite, visualmente oculta]            ← falado: na pausa; frase completa ("Restam 313 caracteres")
```

Estados: normal (cinza `--seed-field-help-text`) → **excedido**: contador em `--seed-field-error-text` + campo `aria-invalid` + mensagem de erro do form-field com o excesso. Formatos de microcopy: "87/400" (visual) · "Restam N caracteres" / "Você ultrapassou o limite em N caracteres" (falado e erro) · erro de submit: "A observação deve ter no máximo 400 caracteres" (padrão NHS: específico, com o limite dito).

### 6.4 Contrato HTML

`<textarea>` nativo sempre (nunca div contenteditable) · SEM `maxlength` (F2) · `rows="3"` como fallback do min-height · `autocapitalize="on" autocorrect="on" spellcheck="true"` (texto humano — o oposto dos identificadores §2.6) · `enterkeyhint` AUSENTE em formulário (Enter quebra linha; "enviar" na tecla seria mentira) e `enterkeyhint="send"` apenas no chat mobile se o produto optar por envio pela tecla.

### 6.5 Código — essência

```css
.seed-textarea{
  field-sizing: content;                 /* auto-grow nativo (F1) */
  min-height: calc(3lh + 20px);
  max-height: calc(8lh + 20px);
  overflow-y: auto;
  resize: vertical;                      /* complemento desktop (F4) */
  padding: 10px 12px;
}
@supports not (field-sizing: content){  /* fallback: técnica grid ::after
  — wrapper .seed-grow com data-value espelhado; estilos idênticos nos dois filhos */ }
```

```tsx
export function Textarea({ limit, ...props }: React.TextareaHTMLAttributes<HTMLTextAreaElement> & { limit?: number }) {
  const { id, helpId, errId } = useField();
  const [len, setLen] = React.useState(String(props.defaultValue ?? "").length);
  const spoken = useDebouncedStatus(limit ? Math.abs(limit - len) : null,
    "", 1000); /* reaproveita o hook da busca §4.10: frase falada só na pausa (F3) */
  const over = limit ? len > limit : false;
  const show = limit ? len >= limit * 0.75 : false; /* threshold (F2) */
  return (<>
    <textarea id={id} rows={3} aria-invalid={over || undefined}
      aria-describedby={cn(over && errId, helpId, limit && `${id}-limit`) || undefined}
      className="seed-textarea w-full rounded-md border border-[var(--seed-field-border)] bg-[var(--seed-field-bg)] text-sm text-[var(--seed-field-value)]"
      onChange={e => { setLen(e.target.value.length); props.onChange?.(e); }} {...props} />
    {limit && <span id={`${id}-limit`} className="sr-only">Limite de {limit} caracteres.</span>}
    {show && <span aria-hidden className={cn("self-end font-mono text-xs",
      over ? "text-[var(--seed-field-error-text)]" : "text-[var(--seed-field-help-text)]")}>{len}/{limit}</span>}
    {limit && <span role="status" className="sr-only">{spoken && (over
      ? `Você ultrapassou o limite em ${len - limit} caracteres`
      : len >= limit * 0.75 ? `Restam ${limit - len} caracteres` : "")}</span>}
  </>);
}
```

### 6.6 Aplicação dos 7 testes (§1.12)

1. Container 3:1: herda §2. ✅ 2. Mecanismos: excedido = cor do contador + aria-invalid + mensagem (nunca só o número vermelho). ✅ 3. Grayscale: excesso legível pelo texto do erro, não pelo matiz. ✅ 4. Par: 87/400 normal × 412/400 excedido distinguíveis a 1s. ✅ 5. Regra do um: um contador, uma live region, uma frase. ✅ 6. Estado atual: quem chega vê quanto foi usado e se estourou, sem digitar. ✅ 7. Polegar 360px: auto-grow respeita o teclado aberto (max-height evita campo-página), fonte 16px, contador visível abaixo sem colidir com o teclado (GOV.UK: abaixo reaparece no scroll natural). ✅

---

