Skip to main content

Rich Textarea

RichTextArea is FormItem + Label + Description + Textarea + Message in one, with an optional character counter. It spreads textarea HTML attributes, so {...field} from react-hook-form works. Everything Headless UI wires inside a Field (for, aria-labelledby, aria-describedby, aria-invalid, disabled) works here too.

import RichTextArea from '@zuilib/components/rich-textarea'

Label and description

Loading example

Character count

showCharCount renders count / maxLength in the footer as a polite live region. Over the limit the counter gets data-state="over", turns --destructive and the control is marked invalid. Note that the native maxLength also prevents typing past the limit; a controlled value set programmatically can still exceed it.

Loading example

Error and unsaved

error renders a Message (role="alert") in the footer and marks the control invalid. unsaved tints the frame --input-dirty. buttonContent sits bottom-right inside the frame.

Loading example

Props

RichTextAreaProps extends the <textarea> attributes (value, defaultValue, onChange, rows, placeholder, disabled, required, …) with:

PropTypeDefaultDescription
labelstringRendered as the Label, associated with the textarea
descriptionstringHelp text, in the textarea's aria-describedby
errorstringRenders a Message (role="alert") in the footer, in aria-describedby; marks the control invalid
showCharCountbooleanfalseShows count / maxLength as a polite live region; data-state="over" and invalid past the limit
maxLengthnumberNative limit and the denominator of the counter
unsavedbooleanfalseTints the frame --input-dirty
buttonContentReactNodeThe Textarea's action slot, bottom-right inside the frame
disabledbooleanfalsePassed to the FormItem so the label and control dim together
classNamestringMerged onto the root FormItem
textareaClassNamestringMerged onto the native <textarea>

The ref points at the native <textarea>.

Slots

SlotElementNotes
[data-slot="rich-textarea"]divRoot (the FormItem)
[data-slot="rich-textarea-label"]label
[data-slot="rich-textarea-description"]p
[data-slot="textarea"]divThe Textarea frame (full width)
[data-slot="textarea-control"]textareaThe native control
[data-slot="textarea-actions"]divWraps buttonContent
[data-slot="rich-textarea-footer"]divPresent with error or showCharCount
[data-slot="rich-textarea-error"]pThe Message, role="alert"
[data-slot="rich-textarea-count"]pThe counter; data-state="over" past the limit
/* Consumer CSS: right-align the error too */
[data-slot="rich-textarea-footer"] {
justify-content: flex-end;
}

Tokens

TokenUsed for
--input, --background, --ringTextarea frame border, fill and focus ring
--input-dirtyunsaved tint
--destructiveError message, invalid border, over-limit counter
--muted-foregroundDescription and counter
--spacingVertical rhythm between the parts

Accessibility

  • The label is associated with the textarea through the FormItem (for / id), so clicking it focuses the field.
  • description, error and the counter are all in the textarea's aria-describedby.
  • error (or an over-limit count) sets aria-invalid on the control; the error is a role="alert" Message.
  • The counter is aria-live="polite", so screen readers announce the count after typing pauses.
  • disabled goes through the FormItem, dimming the label and disabling the control natively.