Textarea
A multi-line box that grows with longer answers. Bios, messages, and free-form notes.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Bio with validation:
```tsx import { Textarea } from '@/components/atoms/Textarea'; import { Field } from '@/components/atoms/Field';
<Field label="Bio" htmlFor="bio" error={err} description="Max 280 characters"> <Textarea id="bio" error={!!err} rows={4} value={bio} onChange={(e) => setBio(e.target.value)} /> </Field> ```
## Accesibilidad
- Always pair with `Field` (or an explicit `<label htmlFor>`). - `error` is visual on the control — put the message string on `Field` for `role="alert"`.
### Correcto
- aria-invalid='true' se agrega automaticamente con `error={true}` - disabled remueve del tab order y desactiva resize - Focus ring visible para navegacion por teclado - Usar dentro de Field para label + mensaje de error asociados via htmlFor/id - rows controla la altura inicial visible — mejor UX que min-height CSS
### Evitar
- No usar placeholder como label — desaparece al escribir - No desactivar resize globalmente — los usuarios necesitan ajustar altura para textos largos - No usar Textarea para input de una linea — usa Input en su lugar
## Cuándo no usar
- Single-line values (email, name) → `Input`. - Structured choices → `Select` / `Combobox`, not a free-text box.
## Criterio de uso
- Define `rows` según la respuesta esperada; un área demasiado pequeña obliga a editar dentro de un viewport incómodo, especialmente en móvil. - Para notas largas, comunica límite o tamaño esperado con `Field` y valida sin borrar texto durante la edición. - Si el valor tiene estructura conocida, usa un control especializado en lugar de pedir formato libre.
## Gotchas
- `error` en `Textarea` cambia la superficie visual; el mensaje humano debe vivir en `Field` para que el error sea anunciado. - Conserva el label visible aunque el placeholder incluya un ejemplo. - **Nota**: La diferencia clave con Input: line-height 1.5 (multi-linea), min-height 80px en vez de height fijo 40px, padding en los 4 lados en vez de solo horizontal, y resize: vertical habilitado. - **Nota**: CSS autocontenido. Mismos colores y focus rings que Input standalone.
Uso
import { Textarea } from '@/components/atoms/Textarea';
<Textarea rows={4} placeholder="Notes" />Props
error: boolean
Gotchas
- a11y
Pair with Field for label/helper/error; set rows thoughtfully for mobile keyboards.
Anatomía CSS
<div class="textarea"> </div>
| Clase | Propósito |
|---|---|
textarea | root |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #fafafa | background |
| all | fg | #525252 | placeholder |
| all | border | #f84131 | destructive |
| all | focused-border | #f84131 | destructive |
| all | disabled-bg | #f5f5f5 | muted |
| all | disabled-fg | #525252 | muted.foreground |
| all | disabled-border | #f5f5f5 | muted |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
border-color | var(--duration-150) | var(--easing-out) |
box-shadow | var(--duration-150) | var(--easing-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Textarea
Multi-line text input. Shares color system with Input.
Min height 80px, resizable vertically by default.
Colors matched to Figma atom-text-field.
States: default, focus, disabled, error
------------------------------------------------------------------------- */
.textarea {
display: flex;
width: 100%;
min-height: 5rem;
padding: 8px;
border: 1.5px solid #d4d4d4;
border-radius: 8px;
background-color: #fafafa;
color: #0a0a0a;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
line-height: 1.5;
resize: vertical;
outline: none;
transition:
border-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}
.textarea::placeholder {
color: #525252;
}
.textarea:focus {
border-color: #171717;
box-shadow: 0 0 0 2px var(--focus-ring-color);
}
.textarea:disabled {
background-color: #f5f5f5;
border-color: #f5f5f5;
color: #525252;
cursor: not-allowed;
resize: none;
opacity: 1;
}
.textarea[aria-invalid="true"] {
border-color: #f84131;
box-shadow: 0 0 2px 0 var(--focus-ring-error-color);
}
.textarea[aria-invalid="true"]:focus {
border-color: #f84131;
box-shadow: 0 0 0 2px var(--focus-ring-error-color);
}
Codigo fuente
import { forwardRef, type TextareaHTMLAttributes } from 'react'; export type TextareaProps = { error?: boolean; className?: string; } & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'>; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>( ({ error = false, className, ...props }, ref) => { return ( <textarea ref={ref} className={cn('textarea', className)} aria-invalid={error || undefined} {...props} /> ); }, ); Textarea.displayName = 'Textarea';