MarkedText
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/marked-text.jsonA textarea cannot colour a word. The way round it is older than React: draw the same text twice, once as marked-up HTML underneath and once as the textarea on top with its own text transparent, so the caret and the selection are the browser’s and the colours are ours.
<MarkedTextarea value={text} onChange={setText} marks={repeats.map((r) => ({ start: r.start, end: r.end, className: 'bg-warn-soft' }))} lineMarks={added.map((line) => ({ line, className: 'bg-good-soft' }))} className="p-3 font-mono text-sm leading-6"/>Both layers take one class list. The two have to agree on every metric —
font, size, line height, padding, wrapping — or the marks slide off the words
they mark. So className is the metrics, given once and put on both; the
textarea adds only what makes it invisible.
The mirror sizes the box. It sits in the flow and the textarea is
stretched over it, so the field grows with its text without a scrollHeight
measurement and never scrolls inside itself — the box around it scrolls, and
the marks scroll with the words.
Two kinds of mark. A Mark is a span of characters, as offsets into the
whole text — a repeated word, a misspelling. A LineMark is a whole line —
one that is new since the other version. A span that crosses a line break is
cut at the break, one <mark> per line; a line is a block, so a line mark
paints the full width and wraps exactly as the textarea’s line does.
A blank line keeps its height. An empty block collapses, and every mark below it lands one line early. The trailing space on the last line is the same rule for a text that ends in a newline.
The <mark> is transparent by default. The browser’s yellow is dropped so
the mark’s own className is its colour — in whichever tone the mark means,
which is the product’s to say.
A mark is { start, end, className?, style? }, offsets into the whole text; a
line mark is { line, className? }, with the line counted from zero.
MarkedText is the same layer to read rather than type into, and
MarkedLines is the layer itself, for a product composing its own box.
Props
Read from the types in ui/marked-text.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
MarkedLines
The marked-up text, line by line. The layer both the readable and the typeable form are made of.
| Prop | Type | Default | |
|---|---|---|---|
textrequired | string | The whole text. Lines are split on \n, and every mark's offsets count into this string. | |
marks | readonly Mark[] | [] | Spans of characters to mark, as offsets into text. Give them in order of start and do not overlap them. |
lineMarks | readonly LineMark[] | [] | Whole lines to mark, by zero-based index. A line mark paints the full width of the line. |
ghost | boolean | false | Transparent text: the textarea above shows the letters. |
MarkedText
Marked text to read.
| Prop | Type | Default | |
|---|---|---|---|
textrequired | string | The whole text. Lines are split on \n, and every mark's offsets count into this string. | |
marks | readonly Mark[] | Spans of characters to mark, as offsets into text. Give them in order of start and do not overlap them. | |
lineMarks | readonly LineMark[] | Whole lines to mark, by zero-based index. A line mark paints the full width of the line. | |
ghost | boolean | Transparent text: the textarea above shows the letters. | |
className | string |
MarkedTextarea
Marked text to type into.
| Prop | Type | Default | |
|---|---|---|---|
ref | Ref<HTMLTextAreaElement> | The ref of the textarea itself, for focus and selection. | |
valuerequired | string | The text, controlled. The field has no state of its own. | |
onChangerequired | (value: string) => void | The new value, not the event: the event is the textarea's business. | |
marks | readonly Mark[] | Spans of characters to mark, as offsets into value. Give them in order of start and do not overlap them. | |
lineMarks | readonly LineMark[] | Whole lines to mark, by zero-based index. | |
className | string | The metrics both layers share: font, size, line height, padding. |
And every prop of React's TextareaHTMLAttributes<HTMLTextAreaElement> except value, onChange, className, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 13 rules applied and passed, and none failed. The accessibility report has every item.
Left to a person, where axe could not decide: color-contrast.
History
Each release that changed ui/marked-text.tsx - the file a product copies - newest first.