Rich Text TextArea Primitive¶
A single TipTap-backed TextArea in @kwilo/ui replaces the plain <textarea> primitive and the forked web-app copy. It keeps the same name, auto-expands by default, makes the toolbar opt-in, and offers a plain mode that strips formatting for chat and short fields. Every role with a text input uses it.
How it works¶
One primitive, one name, one place. Consumers swap with a single prop-signature change: onChange(e) → onChange(value). An RHF TextAreaRHF Controller wrapper covers form usage.
Behavior:
mode='rich'(default) emits HTML viaeditor.getHTML(); the toolbar shows per thetoolbarprop.mode='plain'emits plain text viaeditor.getText(); toolbar suppressed; paste-as-plain.autoGrow=true(default) grows the editor box with content betweenminRowsandmaxRows, then scrolls.maxLengthrenders a counter, warning at 90%, error at 100%.- Disabled, label, hint, error, and aria-* mirror the existing TextArea primitive.
Stack: TipTap 3.23.4 (already in apps/web), StarterKit + Placeholder + Underline + Link + CharacterCount, added as peerDependencies of @kwilo/ui. react-hook-form 7.74 is also a peer.
API¶
type TTextAreaProps = {
value: string // HTML when rich, plain text when plain
onChange: (value: string) => void
onBlur?: () => void
onKeyDown?: (e: KeyboardEvent) => void
label?: string
error?: string
hint?: string
placeholder?: string
minRows?: number // default 2
maxRows?: number // default 12
maxLength?: number
autoGrow?: boolean // default true
disabled?: boolean
required?: boolean
toolbar?: 'none' | 'minimal' | 'full' // default 'minimal'
mode?: 'plain' | 'rich' // default 'rich'
id?: string
name?: string
className?: string
autoFocus?: boolean
}
TextAreaRHF is a thin Controller wrapper. Props: name, control, rules, plus all TextArea props minus value/onChange/onBlur.
Notes¶
- Long-form rich fields start storing HTML. Backend TEXT columns accept it today, but a sanitization pass is needed before render to prevent XSS. Tracked separately.
- Homework long-answer learner submissions default to plain (less attack surface); trainer-authored questions and explanations default to rich.
- Out of scope: migrating
ContentEditor/NotionEditorthemselves, AI inline suggestions, switching to Lexical, markdown serialization, andapps/site(dep-lean, no TipTap).
Where it lives¶
packages/ui/src/primitives/TextArea.tsx: the primitive (replaces the plain version and the forkedapps/web/src/components/ui/TextArea.tsx)- Early consumers:
apps/web/src/pages/shared/AITutorPage/components/ChatInput/index.tsx,apps/web/src/components/messaging/MessageInput.tsx, and the sixapps/web/src/components/homework/QuestionForms/*QuestionForm.tsx