rc-textarea
Textarea wrapper with line decorations, gutter rendering, folding actions, and plugin hooks.
- Package
@rcarls/rc-textarea- Element
<rc-textarea>- Native dependency
- Requires a native textarea child
- State model
- Native textarea value
- Main events
rc-textarea-focusrc-textarea-blurrc-textarea-changerc-textarea-select
Installation
- npm
- Yarn
- pnpm
npm install @rcarls/rc-webcomponents@next
yarn add @rcarls/rc-webcomponents@next
pnpm add @rcarls/rc-webcomponents@next
import '@rcarls/rc-webcomponents/rc-textarea/define';
Usage
Place a native <textarea> as the direct child. It is hidden after upgrade and keeps the form
value, name, and validation; its label names the editor.
<label for="message">Message</label>
<rc-textarea line-numbers word-wrap auto-grow>
<textarea id="message" name="message" rows="10" placeholder="Start typing..."></textarea>
</rc-textarea>
Live demo
- Basic
- Markdown
- Syntax Highlighting
- Expandable lines
- Content language
Plain textarea with auto-grow and no gutter. This is the minimal starting point.
<rc-textarea auto-grow>
<textarea rows="4" aria-label="Text editor"></textarea>
</rc-textarea>
Uses @rcarls/rc-textarea-plugin-markdown for live Markdown decoration, gutter folding of heading sections and multiline blocks, and an HTML preview panel below the editor. Folded source remains in the form value.
import { createMarkdownPlugin } from '@rcarls/rc-textarea-plugin-markdown';
const editor = document.querySelector('rc-textarea');
const plugin = createMarkdownPlugin();
editor.usePlugin(plugin);
// Render preview HTML from the current value
previewEl.innerHTML = plugin.getPreviewHtml(editor.value);
Uses highlight.js with the Rust language pack. Theme colors injected into the shadow root via api.adoptStyleSheet().
import hljs from 'highlight.js/lib/core';
import rust from 'highlight.js/lib/languages/rust';
hljs.registerLanguage('rust', rust);
editor.usePlugin({
mount(api) {
api.adoptStyleSheet(`
.hljs-keyword, .hljs-type { color: #cba6f7; }
.hljs-string { color: #a6e3a1; }
.hljs-comment { color: #6c7086; font-style: italic; }
`);
},
highlight(value) {
return hljs.highlight(value, { language: 'rust' }).value;
},
});
Expand information below a line without changing the textarea's source text.
See Folding actions for controller and styling details.
An Arabic recipe with an English line in an English interface. The textarea's lang and dir
describe the text, and line-direction="auto" aligns each line by its own script. The line
with only a number keeps the Arabic base direction. Line numbers follow the interface.
<rc-textarea line-numbers line-direction="auto">
<textarea aria-label="Recipe directions" lang="ar" dir="auto"></textarea>
</rc-textarea>
Options
Attributes
| Attribute | Property | Description |
|---|---|---|
line-numbers | lineNumbers | Show sequential line numbers in the gutter. |
gutter | gutter | Show an empty gutter that plugins can populate with LineDecoration.gutterContent. |
word-wrap | wordWrap | Wrap long lines instead of scrolling horizontally. |
auto-grow | autoGrow | Let the field grow vertically with content. |
line-direction | lineDirection | base (default) gives every line the editor's direction; auto gives each line its own. |
Read-only state comes from the slotted native control: set readonly on the <textarea>. The
editor keeps the enabled field appearance, stays focusable and selectable, announces
aria-readonly, and rejects edits. The host reflects data-readonly, and the readOnly getter
reports the state.
Use a plugin with LineDecoration.gutterContent for sparse numbering. Put accessible names on
the slotted textarea with aria-label or a real <label for="...">.
Content language and direction
The slotted <textarea> describes the content, and the host describes the interface. Put the
text's language and direction on the textarea, as on any form control:
langis mirrored onto the editor, so the browser picks the right glyphs (Han characters differ betweenjaandzh), spelling dictionary and hyphenation. Plugins read it ascontentLang, andgetWordAtCursor()segments words for it.dirsets the editor's base direction.ltrandrtlare used as written.autotakes the direction of the value's first strong character and uses LTR when there is none, as a native textarea does, and updates as the value changes. Withoutdirthe editor inherits.dirnamesubmits the resolved direction with the form, since the native control always holds the value.spellcheckis mirrored when the textarea sets it. Without it, spelling checks stay off.
Changing the textarea's lang or dir also reruns plugin rendering, so highlighting and
decorations can follow the new content language or direction.
The host's own lang is the interface language, as for every rc-* component. It formats line
numbers, so an ar-EG interface shows ١ ٢ ٣, and ar-EG-u-nu-latn keeps Latin digits. The
gutter, fold controls and line-action popover follow the host's direction, not the content's.
line-direction="auto" suits prose that mixes scripts, such as an Arabic recipe with an
English brand name on one line. Each line with a letter orders and aligns by its own first
strong character (unicode-bidi: plaintext). Lines without one, such as an empty line or a
quantity, keep the base direction, so the caret on a new line in an Arabic recipe starts on the
right. Type U+200E or U+200F at the start of a line to force its direction. Leave it at base
for code. Widgets are isolated, so they never reorder the text around them or set a line's
direction.
JavaScript properties
| Property | Type | Description |
|---|---|---|
value | string | Current plain-text value. Host writes are silent. |
defaultValue | string | undefined | Initial uncontrolled value. |
plugin | RCTextareaPlugin | null | Declarative plugin hook for reactive frameworks. |
lineDirection | 'auto' | 'base' | How each line's direction is chosen; reflects line-direction. |
decorations | DecorationInput[] | External decoration layer merged with plugin and pattern decorations. |
selectionStart / selectionEnd | number | Current plain-text selection offsets (read-only). |
Events
All events bubble and are composed.
| Event | Detail | Fires when |
|---|---|---|
rc-textarea-change | { value: string } | User editing changes the plain-text value. |
rc-textarea-focus | none | The editor receives focus. |
rc-textarea-blur | none | The editor loses focus. |
rc-textarea-select | { selectionStart: number, selectionEnd: number } | The selection changes while the editor is focused. |
editor.addEventListener('rc-textarea-change', (e) => {
console.log(e.detail.value);
});
Accessibility
The editor is a role="textbox" with aria-multiline="true". It takes its accessible name from
the native textarea's aria-label, else the text of the textarea's labels.
| Key | Behavior |
|---|---|
Tab | Inserts \t. |
Escape, then Tab | Moves focus to the next element instead of inserting a tab. |
Shift+Tab | Moves focus to the previous element. |
Ctrl/Cmd+Z | Undo. |
Ctrl/Cmd+Y / Ctrl/Cmd+Shift+Z | Redo. |
Paste | Inserts plain text only; normalizes line endings. |
Enter | Inserts a line break. |
While the editor contains an accessible widget, such as an expanded folding panel, Tab moves
focus instead of inserting a tab, so the widget stays reachable.
Theming
rc-textarea is design-system neutral and uses CSS system colors by default.
Component tokens
| Token | Default | Use |
|---|---|---|
--rc-textarea-font-family | monospace | Editor and gutter font family. While unset, the editor copies the native <textarea>'s font, line height, and spacing. |
--rc-textarea-font-size | 1em | Editor and gutter font size. |
--rc-textarea-line-height | 1.5 | Editor and gutter line height. |
--rc-textarea-padding-block | 0.5em | Editor and gutter block padding; any padding-block value. |
--rc-textarea-padding-inline | 0.5em | Editor inline padding (both sides) and gutter start padding; one length, which the active line highlight bleeds through. |
--rc-textarea-gutter-min-inline-size | 24px | Minimum gutter width including padding; Material uses 48px. |
--rc-textarea-background | Field | Field background. |
--rc-textarea-color | var(--rc-field-text, FieldText) | Field text color. |
--rc-textarea-caret-color | var(--rc-textarea-color, FieldText) | Caret color. |
--rc-textarea-border | 1px solid ButtonBorder | Field border. |
--rc-textarea-border-radius | 2px | Field corner radius. |
--rc-textarea-focus-outline | 2px solid Highlight | Focus ring. |
--rc-textarea-active-line-bg | transparent | Full-width active line highlight. |
--rc-textarea-gutter-bg | Canvas | Gutter background. |
--rc-textarea-gutter-color | GrayText | Gutter text color. |
--rc-textarea-gutter-border | 1px solid ButtonBorder | Gutter separator. |
--rc-textarea-gutter-padding-inline-end | 0.75em | Space between gutter labels and text. |
Plugin-provided line actions expose the --rc-line-actions-* and
--rc-line-action-* hooks listed in the generated API table. Inline actions
inherit the editor surface by default; popover actions use system-color
surface defaults where they need independent chrome.
rc-textarea {
color-scheme: dark;
--rc-textarea-font-family: 'Fira Code', monospace;
--rc-textarea-font-size: 13px;
--rc-textarea-background: #1e1e2e;
--rc-textarea-color: #cdd6f4;
--rc-textarea-active-line-bg: rgb(255 255 255 / 0.06);
}
CSS parts
The exposed CSS parts are root, gutter, gutter-cells, editor-area, editor,
fold-trigger, and fold-icon. The last two exist when a folding action is rendered;
fold-icon exists only when its plugin supplies an icon.
rc-textarea::part(editor) {
tab-size: 2;
}
rc-textarea::part(fold-trigger) {
border-radius: 0.25em;
color: CanvasText;
}
Decoration styles
Decoration elements (.mark, .line, .widget) live inside the shadow root. Use
api.adoptStyleSheet() from a plugin to reach them; document stylesheets cannot pierce the
shadow boundary.
Pattern Highlights
Use addPattern() for lightweight regex decoration without writing a plugin.
const todoId = editor.addPattern({
pattern: /\bTODO\b/g,
bold: true,
color: 'var(--editor-todo-color)',
});
editor.removePattern(todoId);
editor.clearPatterns();
Patterns can also style named capture groups and add line diagnostics:
editor.addPattern({
pattern: /^(?<key>\w[\w-]*):\s*(?<value>.+)$/gm,
captureGroups: {
key: { bold: true, color: 'var(--editor-key-color)' },
value: { color: 'var(--editor-value-color)' },
},
createLineDecoration: () => ({ className: 'config-line' }),
});
Folding actions
Import FoldingActionController from @rcarls/rc-textarea/folding or
@rcarls/rc-webcomponents/rc-textarea/folding when using the aggregate package.
FoldingActionController creates independent native buttons with aria-expanded.
Give each action a stable key, a contextual label, and a target: an app-owned panel
element or { fromLine, toLine } for inclusive, one-based source lines after its
anchor. Source folds start expanded; set initiallyExpanded: false for diagnostic
panels. The controller manages panel visibility and focus return; plugins create
panel content as an accessible block widget and version its widget key when content
changes. clear() resets folding state when document identity changes or the plugin
is destroyed. A Markdown plugin can supply parser-backed ranges without teaching
the editor Markdown grammar.
Supply an icon with the controller's createTriggerIcon(expanded) option. The
icon is decorative and receives the fold-icon CSS part. The built-in Markdown
plugin forwards the same option through its factory:
const plugin = createMarkdownPlugin({
folding: {
createTriggerIcon(expanded) {
const icon = document.createElement('span');
icon.textContent = expanded ? '▾' : '▸';
return icon;
},
},
});
Use ::part(fold-trigger) and ::part(fold-icon) from consumer CSS to style the
control and icon. Keep focus visibility and the button's hit area intact.
gutterLine places a control in the gutter; a trailing offset uses the reserved
lane sized by --rc-textarea-trailing-size. createGutterContent(button) and
updateGutterContent(wrapper) add and refresh a decorative status marker. The
gutter has a 24px minimum width in the base component and 48px in Material.
Each row follows its text line height, so an inline folding control can be shorter
than 24px under WCAG 2.5.8's line-height exception.
The full gutter cell is the hit area for a single action; multiple actions share
the cell without overlapping targets. Enter and Space activate the native button.
Folding never changes the field value or undo history.
Plugins
Plugins add marks, line decorations, and widgets over the plain-text value. See
rc-textarea plugins for the plugin API, helpers, and recipes. The
editor internals are documented for contributors in
ARCHITECTURE.md.
API
Properties
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
lineNumbers | line-numbers | boolean | false | Show sequential line numbers in the gutter. Enables the gutter implicitly. |
gutter | gutter | boolean | false | Enable the gutter column without any built-in content. Plugins can populate individual cells via `LineDecoration.gutterContent`. For sequential line numbers use `lineNumbers` / `line-numbers` instead. |
wordWrap | word-wrap | boolean | false | Wrap long lines within the field to prevent horizontal overflow. |
autoGrow | auto-grow | boolean | false | Allow the field to grow vertically with content to prevent vertical overflow. |
lineDirection | line-direction | 'auto' | 'base' | 'base' | How each line's direction is chosen. `base` gives every line the editor's base direction, which suits code. `auto` gives a line with a letter the direction of its first strong character, for prose that mixes scripts, and holds lines without one (empty, digits, punctuation) to the base direction. |
readOnly | JS property only | boolean | Not specified | Whether the slotted `<textarea>` is `readonly`. The native control is the source of truth; set `readonly` on it to change this state. |
plugin | JS property only | RCTextareaPlugin | null | Not specified | Declarative plugin hook for framework integrations. |
decorations | JS property only | DecorationInput[] | Not specified | Imperatively set an external layer of decorations. Ideal for reactive frameworks. ```tsx // Solid <rc-textarea decorations={decorations()} /> // React 19+ <rc-textarea decorations={decorations} /> ``` Merges with plugin decorations and pattern decorations on every render. Setting this property triggers a new render; setting it to `undefined` or `[]` clears any previously set external decorations. |
value | value | string | Not specified | The current field value. Setting this property puts the component into controlled mode (host owns the value, changes are reflected immediately, no change events dispatched) |
defaultValue | default-value | string | undefined | Not specified | Initial uncontrolled value. |
Methods
| Method | Description |
|---|---|
wrapSelection(prefix: string, suffix: string) | Wrap the current selection with `prefix` and `suffix`. No-op when the selection is collapsed (no text selected). Uses the model path directly (not `execCommand`) because callers such as toolbar buttons move focus away from the editor before this fires, which clears the DOM selection. `_savedSelection` retains the last model-level anchor/focus offsets. |
replaceSelection(text: string) | Replace the current selection with `text`. When the selection is collapsed this is equivalent to `insertText`. |
clearHistory() | Reset the undo/redo history. Call when the field receives entirely new content. |
usePlugin(plugin: RCTextareaPlugin) | Register and mount a plugin imperatively. Replaces any currently active plugin. Prefer the `plugin` property for reactive-framework integrations. |
removePlugin() | Unmount the active plugin, clear its decorations, and release its stylesheets. |
addPattern(pattern: Omit<TextPattern, 'id'>) | Register a text pattern that generates decorations on every render pass. Returns the pattern ID, which can be passed to `removePattern` to unregister it. |
removePattern(id: string) | Remove a previously registered pattern by the ID returned from `addPattern`. |
clearPatterns() | Remove all registered patterns and trigger a re-render. |
Events
| Event | Detail type | Description |
|---|---|---|
rc-textarea-focus | CustomEvent | Fired when the field gains focus |
rc-textarea-blur | CustomEvent | Fired when the field loses focus |
rc-textarea-change | CustomEvent | Fired when the field value changes |
rc-textarea-select | No detail type documented | Fired when the selection changes while the editor is focused |
Slots
| Name | Description |
|---|---|
(default) | Accepts a native `<textarea>` element for form wiring. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-textarea-border | 1px solid ButtonBorder | Border around the field |
--rc-textarea-border-radius | 2px | Border radius of the field |
--rc-textarea-background | Field | Background color of the field |
--rc-textarea-color | var(--rc-field-text, FieldText) | Text color |
--rc-textarea-font-family | monospace | Font family. While it is unset on the host, the editor copies the native `<textarea>`'s font, line height, and spacing as inline styles, so the enhanced editor matches the native control's typography. |
--rc-textarea-font-size | 1em | Font size |
--rc-textarea-line-height | 1.5 | Line height |
--rc-textarea-padding-block | 0.5em | Block padding of the editor and gutter; any `padding-block` value |
--rc-textarea-padding-inline | 0.5em | Inline padding on both sides of the editor and before the gutter; one length, which the active line highlight bleeds through |
--rc-textarea-gutter-min-inline-size | 24px | Minimum width of the gutter, including its padding |
--rc-textarea-trailing-size | 2.5em | Reserved inline space and minimum block size for trailing controls |
--rc-textarea-focus-outline | 2px solid Highlight | Focus ring outline |
--rc-textarea-caret-color | FieldText | Caret color |
--rc-textarea-active-line-bg | transparent | Active line highlight color, including editor and gutter padding |
--rc-textarea-gutter-bg | Canvas | Gutter background color |
--rc-textarea-gutter-color | GrayText | Gutter text color |
--rc-textarea-gutter-border | 1px solid ButtonBorder | Gutter right border |
--rc-textarea-gutter-font-family | var(--rc-textarea-font-family, monospace) | Gutter font family |
--rc-textarea-gutter-content-gap | 0.25em | Gap between a retained line number and custom gutter content |
--rc-textarea-gutter-padding-inline-end | 0.75em | Space between gutter labels and text |
--rc-line-actions-gap | 2px | Gap between inline line actions; popover actions default to 4px |
--rc-line-actions-margin-start | 6px | Inline-start margin before line actions |
--rc-line-action-padding | 1px | Inline line-action padding; popover actions default to 4px 6px |
--rc-line-action-border | none | Line-action border; the popover surface defaults to 1px solid ButtonBorder |
--rc-line-action-border-radius | 3px | Line-action border radius; the popover surface defaults to 4px |
--rc-line-action-bg | transparent | Line-action background; the popover surface defaults to Canvas |
--rc-line-action-color | inherit | Line-action color; popover actions default to ButtonText |
--rc-line-action-font-size | 1em | Inline line-action font size; popover actions default to 0.85em |
--rc-line-action-opacity | 0.4 | Resting opacity of inline line actions |
--rc-line-action-hover-opacity | 1 | Hover and focus opacity of inline line actions |
--rc-line-action-hover-bg | transparent | Hover and focus background; popover actions default to ButtonFace |
--rc-line-action-hover-color | inherit | Hover and focus text color |
--rc-line-action-shadow | 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent) | Line-action popover shadow |
CSS Parts
| Part | Description |
|---|---|
root | The outer flex container wrapping the gutter and editor area. |
gutter | The gutter column container. |
gutter-cells | The gutter's line-label list. |
fold-trigger | Native button created by a folding action; style its visual appearance without changing its target or keyboard behavior. |
fold-icon | Consumer-supplied icon inside a folding action button, when provided. |
editor-area | The container positioning the editor over the slotted textarea. |
editor | The contenteditable field surface. |