Skip to content

MarkedText

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/marked-text.json
See it liveA verse with a repeated word marked and a new line painted, read-only and then as a textarea you can type into.→

A 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.

PropTypeDefault
textrequiredstringThe whole text. Lines are split on \n, and every mark's offsets count into this string.
marksreadonly Mark[][]Spans of characters to mark, as offsets into text. Give them in order of start and do not overlap them.
lineMarksreadonly LineMark[][]Whole lines to mark, by zero-based index. A line mark paints the full width of the line.
ghostbooleanfalseTransparent text: the textarea above shows the letters.

MarkedText

Marked text to read.

PropTypeDefault
textrequiredstringThe whole text. Lines are split on \n, and every mark's offsets count into this string.
marksreadonly Mark[]Spans of characters to mark, as offsets into text. Give them in order of start and do not overlap them.
lineMarksreadonly LineMark[]Whole lines to mark, by zero-based index. A line mark paints the full width of the line.
ghostbooleanTransparent text: the textarea above shows the letters.
classNamestring

MarkedTextarea

Marked text to type into.

PropTypeDefault
refRef<HTMLTextAreaElement>The ref of the textarea itself, for focus and selection.
valuerequiredstringThe text, controlled. The field has no state of its own.
onChangerequired(value: string) => voidThe new value, not the event: the event is the textarea's business.
marksreadonly Mark[]Spans of characters to mark, as offsets into value. Give them in order of start and do not overlap them.
lineMarksreadonly LineMark[]Whole lines to mark, by zero-based index.
classNamestringThe 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.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.36.0
    • fix set typed text at 16px under a finger 967db1d
  3. v0.26.0
    • feat add text with marks, to read or to type into f52f6e1