TextareaField
TextareaField is Field + Label + FieldDescription + Textarea +
FieldError in one, with an optional character counter. It spreads
textarea HTML attributes, so {...field} from react-hook-form works.
Everything a Field wires (for, aria-labelledby, aria-describedby,
aria-invalid, disabled) works here too.
import TextareaField from '@zuilib/primitives/textarea-field'
Label and description
Character count
showCharacterCount renders count / maxLength in the footer as a polite
live region. Over the limit the counter gets data-state="over", turns
--danger 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.
Error and unsaved
error renders a FieldError (role="alert") in the footer and marks the
control invalid. unsaved tints the frame --input-unsaved.
buttonContent sits bottom-right inside the frame.
Props
TextareaFieldProps extends the <textarea> attributes (value,
defaultValue, onChange, rows, placeholder, disabled, required,
…) with:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Rendered as the Label, associated with the textarea |
description | string | — | Help text, in the textarea's aria-describedby |
error | string | — | Renders a FieldError (role="alert") in the footer, in aria-describedby; marks the control invalid |
showCharacterCount | boolean | false | Shows count / maxLength as a polite live region; data-state="over" and invalid past the limit |
maxLength | number | — | Native limit and the denominator of the counter |
unsaved | boolean | false | Tints the frame --input-unsaved |
buttonContent | ReactNode | — | The Textarea's action slot, bottom-right inside the frame |
disabled | boolean | false | Passed to the Field so the label and control dim together |
className | string | — | Merged onto the root Field |
textareaClassName | string | — | Merged onto the native <textarea> |
The ref points at the native <textarea>.
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="textarea-field"] | div | Root (the Field) |
[data-slot="textarea-field-label"] | label | — |
[data-slot="textarea-field-description"] | p | — |
[data-slot="textarea"] | div | The Textarea frame (full width) |
[data-slot="textarea-control"] | textarea | The native control |
[data-slot="textarea-actions"] | div | Wraps buttonContent |
[data-slot="textarea-field-footer"] | div | Present with error or showCharacterCount |
[data-slot="textarea-field-error"] | p | The FieldError, role="alert" |
[data-slot="textarea-field-count"] | p | The counter; data-state="over" past the limit |
/* Consumer CSS: right-align the error too */
[data-slot="textarea-field-footer"] {
justify-content: flex-end;
}
Tokens
| Token | Used for |
|---|---|
--input, --background, --ring | Textarea frame border, fill and focus ring |
--input-unsaved | unsaved tint |
--danger | Error text, invalid border, over-limit counter |
--muted-foreground | Description and counter |
--spacing | Vertical rhythm between the parts |
Accessibility
- The label is associated with the textarea through the
Field(for/id), so clicking it focuses the field. description,errorand the counter are all in the textarea'saria-describedby.error(or an over-limit count) setsaria-invalidon the control; the error is arole="alert"FieldError.- The counter is
aria-live="polite", so screen readers announce the count after typing pauses. disabledgoes through theField, dimming the label and disabling the control natively.
Related
- Textarea: the bare control this wraps.
- Field, Label, FieldDescription, FieldError: the parts, for a custom layout.
- Input: the single-line field.