Skip to content
omaris

Inputs

Rich Text Editor

A rich text editor that is a contenteditable and a toolbar, and nothing else.

import { RichTextEditor } from 'omaris'
Learn

Examples

Basic

A contenteditable with a real toolbar on it. value is HTML, bindable, and sanitised on the way in and out — paste from a word processor and the fonts and colours are stripped on arrival.

<script lang="ts">	import { RichTextEditor } from 'omaris';​	let value = $state(		'<p>The kitchen closes at <strong>11pm</strong>, but delivery keeps going until midnight.</p>'	);</script>​<RichTextEditor bind:value placeholder="Describe the issue…" class="w-full" />

Tools and status

tools picks the buttons and their order — 'separator' draws a divider — so a comment box can have three and a description field twelve. status counts words and characters along the bottom.

8 words 48 characters
<h3>Release notes</h3><ul><li>Docs pages now show the components.</li></ul>
<script lang="ts">	import { RichTextEditor, Text, type RichTextTool } from 'omaris';​	const COMMENT_TOOLS: RichTextTool[] = ['bold', 'italic', 'separator', 'bulletList', 'link'];​	let comment = $state('');	let html = $state('<h3>Release notes</h3><ul><li>Docs pages now show the components.</li></ul>');</script>​<div class="flex w-full flex-col gap-4">	<RichTextEditor		bind:value={comment}		tools={COMMENT_TOOLS}		size="sm"		height="5rem"		placeholder="Leave a comment…"	/>​	<RichTextEditor bind:value={html} status height="10rem" />​	<Text variant="label-sm" tone="muted" font="mono" class="break-all">{html}</Text></div>

Overridden

The rendered HTML is styled through classes.content, not a stylesheet, so a consumer can replace any of it. readOnly keeps the same surface for displaying content that was written elsewhere.

read-only, same surface
<script lang="ts">	import { RichTextEditor, Text } from 'omaris';​	const DOC = `<h3>Refund policy</h3><p>A refund is offered when the order never arrived, or arrived cold.</p><blockquote>Anything past 24 hours goes to support first.</blockquote><ul><li>Card refunds settle in 3–5 days.</li><li>Cash refunds are handled by the courier.</li></ul>`;​	let value = $state(DOC);</script>​<div class="flex w-full flex-col gap-4">	<RichTextEditor		bind:value		height="auto"		classes={{			root: 'rounded-shape-lg border-none bg-surface-container-low',			toolbar: 'bg-transparent',			content:				'min-h-40 [&_h3]:text-title-lg [&_blockquote]:border-s-4 [&_blockquote]:border-primary'		}}	/>​	<div class="flex flex-col gap-1">		<Text variant="label-sm" tone="muted">read-only, same surface</Text>		<RichTextEditor value={DOC} readOnly height="auto" classes={{ content: 'min-h-0' }} />	</div></div>

When to use it

Use it for

  • A description, a note or a comment with bold, a list and a link in it. value is HTML, sanitised on the way in and out.
  • A comment box: size="sm", tools cut to bold, italic and link, a short height.
  • Content written elsewhere and shown here: readOnly. Same typography, same tokens, no toolbar.
  • Paste from a word processor. It arrives as paragraphs, not as spans carrying someone else's font.

Not for

  • Plain text → Textarea.
  • Code, JSON, a query → Code Editor.
  • One line → Text Field.
  • Collaborative editing, custom block types or an enforced schema → not in this library. That is ProseMirror or TipTap.
  • Markdown source edited as markdown → Code Editor with lang="md" and wrap.

Do

  • Keep sanitize on. When you need one more tag, like <table> or <mark>, add it with allowedTags instead of turning the allowlist off.
  • Trim tools to what the field needs. Three buttons for a comment, the full set for a product description.
  • Set status on a field with a limit, so the count is visible.
  • Size it with height. It is a minimum and a property, so classes.content can replace it.

Don't

  • Trust the HTML on the server because the client cleaned it. Run an allowlist on the way into the database too.
  • Set sanitize={false} for content that is "already clean". It is only clean until the first paste.
  • Use it for a title or a name. One line, no formatting → Text Field.

Quick reference

size
  • sm
  • md (default)

API

RichTextEditor

A rich text editor that is a contenteditable and a toolbar, and nothing else.

There is no document model here, and that is the design. A model-backed editor (ProseMirror, Lexical, TipTap) is the right answer for collaborative editing, custom node types or a schema you must enforce — and it is 100–300 kB, which is more than this entire library. For the job most apps actually have — a description, a note, a comment, with bold and a list in it — the browser already ships an editor, and this puts a proper toolbar and a themed surface on it.

The parts that are usually got wrong are handled:

- Paste is cleaned. Pasted HTML goes through an allowlist, so a paragraph from a word processor arrives as a paragraph and not as forty spans carrying somebody else's fonts and colours. - `value` is sanitised the same way before it is put on screen, so round-tripping content through a database cannot smuggle a script in. - The caret survives. The DOM is only rewritten when value really differs from what is in the box, so typing never resets the cursor. - The output is styled by tokens. Headings, lists and quotes are themed with the same scale as the rest of the library, in both themes.

import { RichTextEditor } from 'omaris'
<RichTextEditor bind:value placeholder="Describe the issue…" />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
toolbar
The button strip.
separator
A hairline between groups of buttons.
tool
One toolbar button.
select
The block-style select.
linkBar
The row that slides in when a link is being added.
linkInput
No description in the source yet.
content
The editable surface. The rendered HTML is styled here rather than in a stylesheet, so a consumer can replace any of it through classes.content.
placeholder
Sits over an empty document.
status
Word and character counts.

Props

value bindable

Defaults to ''

string

The document, as HTML. Bindable.

size

Defaults to 'md'

RichTextEditorSize
sm
md
tools

Defaults to DEFAULT_TOOLS

RichTextTool[]

Which buttons, and in what order. 'separator' draws a divider.

placeholder

Defaults to 'Write something…'

string
readOnly

Defaults to false

boolean
status

Defaults to false

boolean

Word and character counts along the bottom.

sanitize

Defaults to true

boolean

Run pasted and incoming HTML through the tag allowlist. Leave it on unless the content is already trusted and already clean.

allowedTags

Defaults to []

string[]

Tags kept by the sanitiser, on top of the defaults.

height

Defaults to '9rem'

number | string

Minimum height of the editing area. A number is px, a string any CSS length — a property rather than a class, so classes.content can replace it.

toolbar
Snippet

Extra buttons, rendered at the end of the toolbar.

onchange
(value: string) => void
class
string
classes
RichTextEditorClasses