Textarea

Pronto

Campo 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

19 / 200

Estados

Default
Filled
Focus (Tab)
Error
Mínimo 10 caracteres.
Disabled

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Border focusfocusborder → borderInk + outline brassEdge150ms ease
Auto-resizeonChangealtura cresce até maxHeight (10 linhas), depois scrollinstant (sem animação)
Counter colorcaracteres > 80% do maxcor inkSubtle → brass; > 100% → carmim150ms ease
Error shakesubmit com errotranslação ±3px horizontal × 2400ms total

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o textarea
EnterQuebra linha (não submete form)
Ctrl+EnterSubmete form pai
EscTira 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

TokenValorPapel
T.surface#FFFFFFbackground
T.border#E3DFD2borda default
T.borderInk#000000borda focus
T.brassEdgergba(164,124,43,0.30)outline ring focus
T.carmim#A32E2Eborda + texto error
RADIUS.md12borderRadius
TYPE.base13fontSize
MOTION.fast150mstransition border