Textarea
ProntoCampo de texto longo (multi-linha) com auto-resize opcional, contador de caracteres e validação inline.
Usar quando
Captura de texto livre acima de ~80 caracteres: descrição, observações, mensagem, fundamentação.
Não usar quando
Texto curto (use Text Input). Editor rico com formatação (use o Editor de Documentos).
Variantes
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Border focus | focus | border → borderInk + outline brassEdge | 150ms ease |
| Auto-resize | onChange | altura cresce até maxHeight (10 linhas), depois scroll | instant (sem animação) |
| Counter color | caracteres > 80% do max | cor inkSubtle → brass; > 100% → carmim | 150ms ease |
| Error shake | submit com erro | translação ±3px horizontal × 2 | 400ms total |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o textarea |
| Enter | Quebra linha (não submete form) |
| Ctrl+Enter | Submete form pai |
| Esc | Tira o foco |
ARIA esperado
- <label htmlFor="obs">Observações</label>
- aria-invalid="true" quando error
- aria-describedby="obs-counter obs-error"
- aria-required="true" se obrigatório
Notas
- Enter quebra linha. NÃO submeta form em Enter (use Ctrl+Enter).
- Auto-resize não pode crescer infinitamente — limite a ~10 linhas + scroll.
- Contador de caracteres é boa prática se houver maxLength real.
- Resize manual (handle ↘) deixe ativo se não houver auto-resize.
Código
'use client';
import { useState } from 'react';
import { T, TYPE, SP, RADIUS, MOTION, transition } from '@/lib/tokens';
export function Textarea({ value, onChange, placeholder, error, maxLength, autoResize, ...rest }: Props) {
const [focused, setFocused] = useState(false);
return (
<div>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={placeholder}
maxLength={maxLength}
rows={autoResize ? 1 : 3}
style={{
width: '100%',
background: T.surface,
border: `1px solid ${error ? T.carmim : focused ? T.borderInk : T.border}`,
borderRadius: RADIUS.md,
padding: `${SP[2]}px ${SP[3]}px`,
fontSize: TYPE.base,
fontFamily: 'inherit',
color: T.ink,
outline: focused ? `2px solid ${T.brassEdge}` : 'none',
resize: autoResize ? 'none' : 'vertical',
minHeight: 80,
maxHeight: 240,
transition: transition('border-color', 'fast'),
}}
/>
{error && <p style={{ fontSize: TYPE.sm, color: T.carmim, marginTop: 4 }}>{error}</p>}
</div>
);
}Regras
Faça
- ✓Use sempre com <label htmlFor> visível acima.
- ✓Default 3 linhas (rows={3}). Cresce até 10 com auto-resize.
- ✓Adicione contador se houver maxLength.
- ✓Permita resize manual (handle ↘) se não houver auto-resize.
- ✓Validação on-blur para mínimo de caracteres.
Não faça
- ✗Não submeta form com Enter — use Ctrl+Enter.
- ✗Não cresça verticalmente sem limite.
- ✗Não use placeholder como label.
- ✗Não force texto sem espaços (deixe quebra natural de palavra).
- ✗Não tire o resize handle (acessibilidade).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background |
T.border | #E3DFD2 | borda default |
T.borderInk | #000000 | borda focus |
T.brassEdge | rgba(164,124,43,0.30) | outline ring focus |
T.carmim | #A32E2E | borda + texto error |
RADIUS.md | 12 | borderRadius |
TYPE.base | 13 | fontSize |
MOTION.fast | 150ms | transition border |