Textarea
Um campo de texto multilinha para capturar conteúdo mais longo, como comentários, descrições ou mensagens. Compartilha convenções de estilo com o componente Input para consistência visual em formulários.
Busque em todas as páginas da documentação
Um campo de texto multilinha para capturar conteúdo mais longo, como comentários, descrições ou mensagens. Compartilha convenções de estilo com o componente Input para consistência visual em formulários.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
interface TextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export function Textarea({ label, value, onChange }: TextareaProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={4}
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</label>
);
}Um textarea rotulado mínimo. O callback onChange retorna o valor da string diretamente para que o componente pai não precise desempacotar e.target.value. Envolver o textarea dentro de um elemento <label> associa o rótulo ao campo automaticamente.
"use client";
interface TextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
error?: string;
}
export function Textarea({ label, value, onChange, error }: TextareaProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={4}
aria-invalid={!!error}
className={`mt-1 block w-full rounded-lg border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 ${
error
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
/>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
</label>
);
}Alterna as cores da borda e do anel para vermelho quando uma mensagem de erro está presente. O atributo aria-invalid informa aos leitores de tela que o campo tem um problema de validação.
"use client";
import { useRef, useCallback } from "react";
interface AutoResizeTextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
minRows?: number;
}
export function AutoResizeTextarea({
label,
value,
onChange,
minRows = 3,
}: AutoResizeTextareaProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(e.target.value);
const el = e.target;
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
},
[onChange]
);
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
ref={textareaRef}
value={value}
onChange={handleChange}
rows={minRows}
className="mt-1 block w-full resize-none rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</label>
);
}O textarea cresce conforme o usuário digita, redefinindo sua altura para auto e depois definindo-a para scrollHeight a cada alteração. A classe resize-none desabilita a alça de redimensionamento manual, já que a altura é gerenciada programaticamente.
"use client";
interface TextareaWithCountProps {
label: string;
value: string;
onChange: (value: string) => void;
maxLength: number;
}
export function TextareaWithCount({
label,
value,
onChange,
maxLength,
}: TextareaWithCountProps) {
const remaining = maxLength - value.length;
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
maxLength={maxLength}
rows={4}
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<p
className={`mt-1 text-right text-xs ${
remaining < 20 ? "text-red-500" : "text-gray-400"
}`}
>
{value.length}/{maxLength}
</p>
</label>
);
}Exibe uma contagem de caracteres em tempo real abaixo do textarea. O contador fica vermelho quando restam menos de 20 caracteres, dando ao usuário um aviso visual antes de atingir o limite. O atributo nativo maxLength impede a ultrapassagem do máximo.
"use client";
interface TextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
helperText?: string;
error?: string;
}
export function Textarea({ label, value, onChange, helperText, error }: TextareaProps) {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={4}
aria-invalid={!!error}
className={`mt-1 block w-full rounded-lg border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 ${
error
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
/>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
{!error && helperText && <p className="mt-1 text-sm text-gray-500">{helperText}</p>}
</label>
);
}O texto de ajuda aparece abaixo do textarea quando não há erro. Quando um erro está presente, ele tem prioridade, evitando que mensagens conflitantes se acumulem.
"use client";
interface TextareaProps {
label: string;
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}
export function Textarea({ label, value, onChange, disabled = false }: TextareaProps) {
return (
<label className="block">
<span
className={`text-sm font-medium ${disabled ? "text-gray-400" : "text-gray-700"}`}
>
{label}
</span>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
rows={4}
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"
/>
</label>
);
}A prop disabled impede a edição e aplica um estilo atenuado através das variantes disabled: do Tailwind. O rótulo também escurece para reforçar o estado inativo.
"use client";
import { useActionState } from "react";
interface FormState {
message: string;
error?: string;
}
async function submitFeedback(
_prev: FormState,
formData: FormData
): Promise<FormState> {
const content = formData.get("content") as string;
if (!content || content.trim().length < 10) {
return { message: "", error: "O feedback deve ter pelo menos 10 caracteres." };
}
// Simula submissão do servidor
return { message: "Obrigado pelo seu feedback!" };
}
export function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {
message: "",
});
return (
<form action={formAction} className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-gray-700">Seu Feedback</span>
<textarea
name="content"
rows={5}
required
className={`mt-1 block w-full rounded-lg border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 ${
state.error
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
}`}
/>
{state.error && <p className="mt-1 text-sm text-red-600">{state.error}</p>}
</label>
<button
type="submit"
disabled={isPending}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Enviando..." : "Enviar"}
</button>
{state.message && <p className="text-sm text-green-600">{state.message}</p>}
</form>
);
}Usa useActionState do React 19 para lidar com a submissão de formulário com validação no lado do servidor. O textarea é não controlado através do atributo name, permitindo que o navegador lide com a coleta de FormData. O sinalizador isPending desabilita o botão de envio durante o processamento assíncrono.
"use client";
import { forwardRef, useId, useRef, useCallback, useEffect } from "react";
type TextareaSize = "sm" | "md" | "lg";
interface TextareaProps
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
label?: string;
helperText?: string;
error?: string;
size?: TextareaSize;
maxLength?: number;
showCount?: boolean;
autoResize?: boolean;
onValueChange?: (value: string) => void;
onChange?: React.ChangeEventHandler<HTMLTextAreaElement>;
fullWidth?: boolean;
}
const sizeClasses: Record<TextareaSize, { input: string; text: string }> = {
sm: { input: "px-2.5 py-1.5 text-xs", text: "text-xs" },
md: { input: "px-3 py-2 text-sm", text: "text-sm" },
lg: { input: "px-4 py-3 text-base", text: "text-base" },
};
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
function Textarea(
{
label,
helperText,
error,
size = "md",
maxLength,
showCount = false,
autoResize = false,
onValueChange,
onChange,
fullWidth = true,
disabled,
className,
id: externalId,
value,
defaultValue,
rows = 4,
...rest
},
ref
) {
const generatedId = useId();
const inputId = externalId ?? generatedId;
const errorId = `${inputId}-error`;
const helperId = `${inputId}-helper`;
const internalRef = useRef<HTMLTextAreaElement>(null);
const textareaRef = (ref as React.RefObject<HTMLTextAreaElement>) ?? internalRef;
const adjustHeight = useCallback(() => {
const el = textareaRef.current;
if (!el || !autoResize) return;
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
}, [autoResize, textareaRef]);
useEffect(() => {
adjustHeight();
}, [value, adjustHeight]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange?.(e);
onValueChange?.(e.target.value);
if (autoResize) {
e.target.style.height = "auto";
e.target.style.height = `${e.target.scrollHeight}px`;
}
},
[onChange, onValueChange, autoResize]
);
const hasError = !!error;
const sizes = sizeClasses[size];
const currentLength =
typeof value === "string"
? value.length
: typeof defaultValue === "string"
? defaultValue.length
: 0;
return (
<div className={fullWidth ? "w-full" : "inline-flex flex-col"}>
{label && (
<label
htmlFor={inputId}
className={`mb-1 block font-medium text-gray-700 ${sizes.text}`}
>
{label}
</label>
)}
<textarea
ref={textareaRef}
id={inputId}
disabled={disabled}
value={value}
defaultValue={defaultValue}
onChange={handleChange}
rows={rows}
maxLength={maxLength}
aria-invalid={hasError}
aria-describedby={
[hasError ? errorId : null, helperText ? helperId : null]
.filter(Boolean)
.join(" ") || undefined
}
className={[
"block rounded-lg border shadow-sm transition-colors",
"focus:outline-none focus:ring-1",
"disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500",
"placeholder:text-gray-400",
autoResize ? "resize-none overflow-hidden" : "resize-y",
sizes.input,
fullWidth ? "w-full" : "",
hasError
? "border-red-500 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500",
className ?? "",
]
.filter(Boolean)
.join(" ")}
{...rest}
/>
<div className="mt-1 flex items-start justify-between gap-2">
<div>
{hasError && (
<p id={errorId} className={`text-red-600 ${sizes.text}`} role="alert">
{error}
</p>
)}
{!hasError && helperText && (
<p id={helperId} className={`text-gray-500 ${sizes.text}`}>
{helperText}
</p>
)}
</div>
{showCount && maxLength && (
<p
className={`shrink-0 ${sizes.text} ${
currentLength >= maxLength * 0.9 ? "text-red-500" : "text-gray-400"
}`}
>
{currentLength}/{maxLength}
</p>
)}
</div>
</div>
);
}
);Aspectos chave:
<label>, aria-describedby e texto de erro/ajuda sem exigir que o consumidor forneça IDs.onChange nativo quanto um callback de string onValueChange simplificado, tornando-o compatível com bibliotecas de formulário e estado simples.autoResize está habilitado, o textarea cresce para caber o conteúdo, redefinindo a altura para auto e depois definindo-a para scrollHeight, com resize-none e overflow-hidden para evitar artefatos visuais.maxLength, dando aos usuários um aviso antecipado antes de atingir o limite.disabled:cursor-not-allowed e um fundo atenuado para que o estado desabilitado seja visualmente óbvio sem depender apenas da opacidade.Confusão entre resize-y e resize-none -- Usar resize-y permite o redimensionamento vertical, mas entra em conflito com o comportamento de redimensionamento automático. Ao usar ajuste de altura programático, sempre combine-o com resize-none para evitar que a alça de redimensionamento entre em conflito com o script.
scrollHeight requer altura auto primeiro -- Definir el.style.height = el.scrollHeight + "px" sem primeiro redefinir para auto faz com que o textarea apenas cresça, nunca encolha. Sempre redefina para auto antes de ler scrollHeight.
Aviso de controle de não controlado para controlado -- Começar com value={undefined} e depois mudar para uma string dispara um aviso do React. Sempre inicialize o estado como uma string vazia ao usar o modo controlado.
maxLength não valida em colagens em todos os navegadores -- Alguns navegadores mais antigos permitem colar além de maxLength. Sempre valide o comprimento no lado do servidor ou em seu manipulador de envio como fallback.
Prop rows ignorada com redimensionamento automático -- Quando o redimensionamento automático está ativo, o atributo rows define apenas a altura inicial. Após a primeira digitação, a altura programática assume o controle. Defina uma min-height no Tailwind se precisar de um mínimo garantido.
Serialização de dados de formulário -- Textareas preservam novas linhas como \r\n em dados de formulário no Windows. Normalize com .replace(/\r\n/g, "\n") se finais de linha consistentes forem importantes para seu backend.
Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥