SvRichText

A lightweight WYSIWYG editor: a formatting toolbar over a contentEditable region that emits HTML through a bindable value.

SvRichText covers the common editing commands - bold, italic, underline, strikethrough, headings, lists, quote, code block, alignment, links, and undo/redo - in a compact toolbar you can reorder or trim. It emits an HTML string via bind:value and reflects the active formatting state (bold/italic and list buttons light up with the caret). It uses document.execCommand, the pragmatic approach for a component-kit editor. All color comes from the --sg-* tokens.

Basic usage

<script lang="ts">
  import { SvRichText } from '@svgrid/grid'
  let html = $state('<p>Hello</p>')
</script>

<SvRichText bind:value={html} placeholder="Write something…" />

Props

Prop Type Default Description
value string '' HTML string (bindable).
onChange (html: string) => void - Fires with the HTML on every edit.
placeholder string '' Shown when the editor is empty.
disabled boolean false Dims the editor and blocks all input.
readonly boolean false Hides the toolbar and makes the body non-editable.
minHeight string 160px Minimum height of the editable body (any CSS length).
tools RichTextTool[] full default set Toolbar tools and their order; use '|' for a separator.
ariaLabel string Rich text editor Accessible name for the editable region.

RichTextTool (exported) is one of: 'bold' | 'italic' | 'underline' | 'strike' | 'h1' | 'h2' | 'h3' | 'p' | 'ul' | 'ol' | 'quote' | 'code' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'link' | 'clear' | 'undo' | 'redo' | '|'. The '|' entry draws a visual separator between tool groups.

Patterns

A trimmed toolbar

Pass just the tools you need; group them with '|' separators:

<SvRichText
  bind:value={note}
  tools={['bold', 'italic', '|', 'ul', 'ol', '|', 'link']}
/>

Read-only rendering

Set readonly to show formatted content without a toolbar - useful for previews of the same HTML the editor produced:

<SvRichText value={savedHtml} readonly />

Reacting to edits

Persist on change instead of only on blur:

<SvRichText bind:value={html} onChange={(h) => queueSave(h)} />

A field bound to a record, with preview

Bind value straight to a field on your state object so edits flow back into the model. Track a dirty flag from onChange to gate the Save button, then flip to readonly to preview the exact HTML the editor produced:

<script lang="ts">
  import { SvRichText, SvButton } from '@svgrid/grid'

  let ticket = $state({ id: 42, notes: '<p>Initial triage notes.</p>' })
  let editing = $state(true)
  let dirty = $state(false)

  function save() {
    fetch(`/api/tickets/${ticket.id}`, {
      method: 'PATCH',
      body: JSON.stringify({ notes: ticket.notes }),
    })
    dirty = false
    editing = false
  }
</script>

{#if editing}
  <SvRichText
    bind:value={ticket.notes}
    tools={['bold', 'italic', '|', 'ul', 'ol', '|', 'link', 'clear']}
    minHeight="200px"
    placeholder="Add triage notes..."
    onChange={() => (dirty = true)}
  />
  <SvButton variant="primary" disabled={!dirty} onclick={save}>Save</SvButton>
{:else}
  <SvRichText value={ticket.notes} readonly />
  <SvButton onclick={() => (editing = true)}>Edit</SvButton>
{/if}

Tip: an external change to value is written into the editor only while it is not focused - writing innerHTML mid-caret would collapse the selection - so a value set from elsewhere applies once the field blurs. readonly mode drops the toolbar and renders the same HTML, making it a zero-cost preview of what you saved.

Accessibility

See also