DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Textarea

Multi-line text input on the Base UI Field contract — shell variants, validation, header/footer slots, counter modes, auto-resize, and LTR/RTL support.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Textarea } from "@tessinaui/ui";
{/* Basic */}
<Textarea label="Description" placeholder="Write something…" />

{/* With character counter */}
<Textarea
  label="Bio"
  maxLength={160}
  showCharacterCounter
  placeholder="Tell us about yourself…"
/>

{/* Validation on the Base UI Field contract */}
<Textarea
  label="Review"
  validate={(v) => (typeof v === "string" && v.length < 20 ? "Too short" : null)}
  validationMode="onChange"
/>

{/* Composer shell */}
<Textarea
  variant="plain"
  label="Add a comment"
  visuallyHiddenLabel
  autoResize
  maxRows={6}
  submitOn="mod-enter"
  footer={<Button size="xs">Send</Button>}
/>

Examples

Default

A basic textarea with a label and placeholder.

Variants

Four shells: the default outline box, a filled well, a dense-form underline rule, and the chrome-free plain composer shell. plain keeps its focus ring, and filled/plain regain a visible boundary whenever the intent is not none.

Sizes

Five sizes from xs to xl. Type follows the input family's shared ladder — 12/14/14/16/16 on desktop with a 16px floor on phones, so iOS never zooms on focus.

Intents

Validation states — error, warning, success, and info — recolor the border, focus ring, and supporting text.

Validation

errorMessage replaces the supporting text and marks the field invalid; validate runs on the Base UI Field contract (onSubmit, onBlur, or debounced onChange).

Character counter

Grapheme-aware counting (an emoji counts once) in three formats — ratio, remaining, count — placed below or inside the box. A softLimit recommends a length without blocking typing; a hard maxLength over-run also sets aria-invalid. Announcements fire in bands, never per keystroke.

Auto-resize

With autoResize, the height grows as you type — capped here at six rows before scrolling. A ResizeObserver keeps the measurement honest across container-width changes and late font loads.

Resize

The drag grip resizes the whole field group — helper rows follow, and the width floors at the widest helper row. maxRows now caps the height even without autoResize.

Label placement

Place the label top, inside the container, or at the start (logical — right side in RTL). optional renders the muted counterpart to required's asterisk.

Composer

The plain shell plus header/footer slots, visuallyHiddenLabel, clearable, and submitOn="mod-enter". Send/stop behaviour, attachments and uploads belong to prompt-input — Textarea supplies the shell.

States

readOnly (focusable, selectable, not editable), disabled, isLoading (edits blocked, aria-busy), and monospace.

On color

Inside a coloured Surface, the well, ink and helper colours derive from the surface ink contract — the same tone model as Field and Button. Each variant keeps its own anatomy on the plate: filled has no resting border, underline focuses with an ink band instead of a ring, and plain stays bare until an intent gives it a boundary.

AI assist

A footer action that fills the field, isLoading while in flight, and a value the user can edit or clear afterwards — the human-in-the-loop recipe.

API Reference

Props

PropTypeDefaultDescription
variant"outline" | "filled" | "underline" | "plain""outline"Shell: bordered box, tinted well, bottom rule, or chrome-free composer
size"xs" | "sm" | "md" | "lg" | "xl""md"Density; type reads the input family's shared ladder with a 16px phone floor
intent"none" | "error" | "warning" | "success" | "info""none"Border, focus-ring and helper colour
rounded"none" | "sm" | "md" | "lg" | "full""full"Token radii (0/6/12/16/24px); full caps at 24px so a tall box never domes
labelReactNode—Label text; names the control via the Base UI Field wiring
labelPlacement"top" | "inside" | "start""top"Logical label position (start flips in RTL)
labelWidth"sm" | "md" | "lg" | "xl" | "auto" | CSS length"md"Width of the start-label column, so sibling fields in a form share one control column — sm 6rem · md 8rem · lg 11rem · xl 14rem. Labels wrap, never truncate. Only applies with labelPlacement="start", from sm up
labelPosition"outside-top" | "inside" | "outside-left"—Deprecated — alias for labelPlacement
requiredboolean—Asterisk + aria-required
optionalboolean—Muted "(optional)" marker; mutually exclusive with required
visuallyHiddenLabelboolean—Keep the label for AT only (composer shells)
infoTextstring—Hint at the label row's end, joined into aria-describedby
supportingTextReactNode—Helper below the field; replaced by the error row while invalid
errorMessageReactNode—Error row; sets aria-invalid, implies intent="error"
validate(value, formValues) => string | string[] | null | Promise—Base UI Field validation callback
validationMode"onSubmit" | "onBlur" | "onChange""onSubmit"When validate runs
validationDebounceTimenumber0Debounce for onChange validation, ms
showCharacterCounterbooleanfalseShows the counter (needs a limit except with counterFormat="count")
counterFormat"ratio" | "remaining" | "count""ratio""12 / 200" · "188 characters remaining" (flips to "N over" past the limit) · "12 characters"
counterPlacement"below" | "inside""below"Footer row, or pinned inside the box's bottom-end
softLimitnumber—Recommendation: warns past it, never blocks typing
maxLengthnumber—Native hard limit (counts UTF-16 code units; the visible counter counts graphemes)
headerReactNode—Slot inside the box, above the text — chips, identity
footerReactNode—Slot inside the box, below the text — actions, toolbars
clearablebooleanfalseClear (×) button while there's a value; Escape clears too
onClear() => void—Called after a clear
isLoadingbooleanfalseSpinner, aria-busy, edits blocked via readOnly
monospacebooleanfalseMonospaced value
submitOn"none" | "mod-enter""none"⌘/Ctrl+Enter submits the closest form; Enter keeps inserting newlines
tone"default" | "on-color"Surface's toneRenders for a coloured/dark plate via the surface ink contract
minRowsnumber3Minimum visible rows (maps to rows)
maxRowsnumber—Cap before scrolling — applies with and without autoResize
autoResizebooleanfalseGrows with content; disables the resize grip
resize"none" | "vertical" | "horizontal" | "both""vertical"Drag-grip axes; the grip resizes the whole field group
value / defaultValue / onChange——Controlled and uncontrolled both supported
onValueChange(value: string) => void—Value-first callback alongside onChange, "" on clear
namestring—Form field name (on the Base UI Field root)
disabled / readOnlyboolean—Native semantics; readOnly stays focusable and selectable
dir"ltr" | "rtl""ltr"Text direction
wrapperClassName / containerClassName / classNamestring—Outer wrapper · bordered container · the <textarea> itself

All other standard <textarea> attributes (placeholder, rows, wrap, spellCheck, autoFocus, enterKeyHint, …) forward to the underlying element. An explicit rows deliberately overrides minRows.

TextareaSkeleton

Same box as the real control at every size (textareaContainerVariants + skeletonShell). Props: size, rounded, variant, minRows, showLabel, showHelper, labelChars, helperChars.

The two *Chars props state how long the real text is, in characters, so the bars measure what the text measures (§2b) — they run through skeletonTextWidth, which corrects ch (the advance of "0") to the average glyph. Defaults: 14 and 28.

Notes

  • Accessibility wiring — the label names the control; supporting text, info text, error and the counter's limit description all reach it via aria-describedby in a fixed order. The error region is a pre-mounted polite live region. Counter announcements fire only when crossing 90% and the limit, debounced — never per keystroke, and never from the counter element itself.
  • Grapheme counting — the visible counter counts user-perceived characters (Intl.Segmenter), so "😄" is 1. Native maxLength counts UTF-16 code units, so the enforced limit can differ from the display; the count freezes during IME composition.
  • Over-limit — never colour-alone: the tint pairs with an icon, and a hard over-run (long defaultValue, controlled value) sets aria-invalid. A field filled to exactly maxLength is not invalid — that is the documented maximum, so the counter shows the ceiling has been reached while validity stays clean.
  • Resize grip — desktop only (hidden below md), and hidden while disabled, read-only or loading. It carries touch-action: none, so on a phone a thumb landing in that corner would resize the field instead of scrolling the page; Fluent 2 defaults resize to none for the same reason. Touch and keyboard users get maxRows / autoResize instead.
  • Shell-less variants stay shell-less — disabled and readOnly repaint the well, they do not invent one: plain gains no plate and no border, underline keeps its rule but gains no fill. Ink, cursor and pointer-events still apply everywhere, so a disabled plain field reads and behaves as disabled; a plain field with an intent boundary mutes that boundary.
  • Error replaces helper — the error row swaps in for supportingText (M3/Carbon) so layout never shifts under an open keyboard; readOnly suppresses the error state entirely.
  • Don't nest in menus — a textarea consumes Up/Down and Enter, so never place one inside a Toolbar, Menu or Listbox pattern; use a dialog or popover.
  • In sheets — present compose sheets at full height (no half detent), per the platform guidance for growing composers.
  • data-slot map — textarea (root) · textarea-container · textarea-input · textarea-label · textarea-info · textarea-supporting · textarea-error · textarea-error-region · textarea-counter · textarea-header · textarea-footer · textarea-clear · textarea-grip · textarea-spinner. Root also stamps data-size, data-intent, data-variant, data-rounded, data-label-placement, data-tone, data-loading, data-readonly.
TabsTime Picker

On this page

PlaygroundInstallationUsageExamplesDefaultVariantsSizesIntentsValidationCharacter counterAuto-resizeResizeLabel placementComposerStatesOn colorAI assistAPI ReferencePropsTextareaSkeletonNotes