Sign in

There's an API for that

Everything Sumaq can do, you can control. Configuration, methods, events, style tokens: it's all here, and it all works the way you'd expect.

Quick Start

import Sumaq from './src/sumaq.js';

const editor = new Sumaq('#my-container', {
  toolbar: ['h1', 'h2', 'p', '|', 'bold', 'italic', '|', 'link'],
  onChange: (html) => console.log(html)
});

The constructor takes a container (CSS selector string or DOM element) and an optional config object. It returns an editor instance. The container's existing HTML content is preserved and loaded into the editor.

Config

Option Type Default Description
toolbar string[] All 25 buttons + separators Button IDs to show, in order. '|' inserts a separator.
styleOverrides string | null null CSS string injected as a second <style> inside Shadow DOM, after the base styles.
onChange function | null null Callback fired on debounced content change. Receives sanitized HTML.
extensions array [] Extension objects. Passed through. See Extensions.

Methods

getHTML()string

Returns the editor's content as sanitized HTML. In source mode, sanitizes the raw textarea value before returning.

const html = editor.getHTML();

setHTML(html)void

Sets the editor's content. The input is sanitized, then normalized (semantic tag conversion, inline style cleanup). Emits a 'change' event.

editor.setHTML('<p>Hello <strong>world</strong></p>');

getText()string

Returns the plain text content (no HTML tags).

const text = editor.getText();

on(event, callback)void

Registers an event listener.

editor.on('change', (html) => { /* ... */ });

off(event, callback?)void

Removes an event listener. If callback is omitted, removes all listeners for that event.

editor.off('change', myHandler);  // remove specific listener
editor.off('change');              // remove all 'change' listeners

destroy()void

Full cleanup. Disconnects mutation observer, closes open dialogs, removes the selectionchange listener, destroys extensions, tears down Shadow DOM, and restores the sanitized HTML content to the original container.

editor.destroy();

Events

'change'

Debounced (150ms). Fires after content settles. Receives sanitized HTML.

editor.on('change', (html) => saveToServer(html));

'input'

Immediate. Fires on every keystroke / content mutation. Receives HTML (raw in source mode).

editor.on('input', (html) => updatePreview(html));

'drop'

Fires when content is dropped into the editor. Default browser drop behavior is prevented. The editor does not insert anything. It hands off the payload for external handling (e.g., CMS asset upload, base64 conversion).

editor.on('drop', ({ files, imageFiles, html, text, originalEvent }) => {
  if (imageFiles.length) {
    uploadToAssetManager(imageFiles).then(urls => {
      urls.forEach(url => {
        const img = document.createElement('img');
        img.src = url;
        img.alt = '';
        // use editor internals or setHTML to insert
      });
    });
  }
});
Property Type Description
filesFile[]All dropped files
imageFilesFile[]Dropped files with image/* MIME type
htmlstringHTML content from the drop data (if any)
textstringPlain text content from the drop data (if any)
originalEventDragEventThe native drop event, for coordinates or additional dataTransfer access

'imagePaste'

Fires when images are pasted from the clipboard (e.g., screenshot paste, copy-pasted image). Default paste behavior is prevented. When no images are in the clipboard, the editor falls back to plain text insertion.

editor.on('imagePaste', ({ files }) => {
  files.forEach(file => {
    const reader = new FileReader();
    reader.onload = () => {
      const img = document.createElement('img');
      img.src = reader.result; // base64
      img.alt = '';
      editor.setHTML(editor.getHTML() + img.outerHTML);
    };
    reader.readAsDataURL(file);
  });
});
Property Type Description
filesFile[]Pasted image files (image/* MIME type only)

Both drop and imagePaste are intentionally CMS-agnostic. The editor provides the data; your integration decides what to do with it. No files are inserted, uploaded, or converted by the editor itself.

Toolbar Buttons

25 core buttons plus '|' as separator.

ID Function Output
h1Heading 1<h1>
h2Heading 2<h2>
h3Heading 3<h3>
h4Heading 4<h4>
h5Heading 5<h5>
h6Heading 6<h6>
pParagraph<p>
preCode block<pre>
blockquoteBlock quote<blockquote> with <cite>
boldBold<strong>
italicItalic<em>
underlineUnderline<u>
strikethroughStrikethrough<s>
superscriptSuperscript<sup>
subscriptSubscript<sub>
markHighlight<mark>
codeInline code<code>
linkInsert/edit link<a href>, dialog with three types: URL (with optional target="_blank"), email (mailto: with optional subject), phone (tel:)
anchorInsert/edit anchor<a id>: ID must start with a letter, only letters/numbers/hyphens/underscores allowed. Validated live in the dialog.
alignLeftAlign leftremoves alignment class
alignCenterCenter.text-center
alignRightAlign right.text-right
ulUnordered list<ul>
olOrdered list<ol>
hrHorizontal rule<hr>
tableTable<table>: dialog for rows/cols (max 10 each). When cursor is inside an existing table, opens in edit mode with current dimensions pre-filled and options to resize or delete.
imageImage<img> (dialog for src, alt)
undoUndobrowser undo
redoRedobrowser redo
sourceSource viewtoggles <textarea> with raw HTML. Switching back to rich text sanitizes the content.
'|'Separatorvisual divider, no function

Style Overrides

The styleOverrides config option injects a second <style> element into the Shadow DOM, after the base styles. Your CSS wins through normal cascade order. No !important needed.

const editor = new Sumaq('#editor', {
  styleOverrides: `
    .sumaq-content h1,
    .sumaq-content h2,
    .sumaq-content h3 {
      font-family: Georgia, 'Times New Roman', serif;
    }
    .sumaq-content a {
      color: oklch(.45 .15 260);
    }
    .sumaq-content blockquote {
      border-left-color: oklch(.6 .1 260);
    }
  `
});

The easiest override path: change --sumaq-* custom properties on :host.

const editor = new Sumaq('#editor', {
  styleOverrides: `
    :host {
      --sumaq-accent: oklch(.5 .2 150);
      --sumaq-border-blockquote: oklch(.5 .2 150);
    }
  `
});

CSS Custom Properties

All tokens use the --sumaq- prefix. Defined on :host, they cascade into every component inside the Shadow DOM.

Surface

Token Default Purpose
--sumaq-bgoklch(.985 .003 107)Editor wrapper background
--sumaq-bg-secondarytransparentToolbar, table header background
--sumaq-bg-tertiaryoklch(.947 .007 81)Escape hint background
--sumaq-bg-activeoklch(.947 .007 81)Button active state
--sumaq-bg-contentoklch(.985 .003 107)Content area background
--sumaq-bg-content-focusoklch(.985 .003 107)Content area on focus
--sumaq-bg-codeoklch(.96 .005 95)Inline <code> background
--sumaq-bg-preoklch(.2 .01 255)<pre> and source view background
--sumaq-bg-markoklch(.94 .035 90)<mark> highlight background
--sumaq-bg-dialogoklch(.985 .003 107)Dialog box background
--sumaq-bg-backdropoklch(0 0 0 / .35)Dialog backdrop overlay

Text

Token Default Purpose
--sumaq-textoklch(.423 .003 107)Primary text color
--sumaq-text-secondaryoklch(.55 .003 107)Secondary text (blockquote, cite)
--sumaq-text-tertiaryoklch(.7 .003 107)Tertiary text (placeholders)
--sumaq-text-codeoklch(.45 .04 255)Inline <code> text color
--sumaq-text-preoklch(.9 .005 255)<pre> and source view text color

Accent

Token Default Purpose
--sumaq-accentoklch(.308 .051 253)Links, caret, primary buttons, active states
--sumaq-accent-hoveroklch(.25 .05 253)Accent hover state
--sumaq-accent-textoklch(.985 .003 107)Text on accent-colored backgrounds

Danger

Token Default Purpose
--sumaq-dangeroklch(.5 .13 30)Delete/remove buttons
--sumaq-danger-hoveroklch(.43 .13 30)Danger hover state

Border

Token Default Purpose
--sumaq-borderoklch(.925 .008 92)Editor wrapper border, dialog inputs
--sumaq-border-hoveroklch(.363 .039 255)Border hover state
--sumaq-border-subtleoklch(.947 .007 81)Dialog header/footer dividers
--sumaq-border-tableoklch(.925 .008 92)Table cell borders, horizontal rule
--sumaq-border-blockquoteoklch(.308 .051 253)Blockquote left border

Radius

Token Default Purpose
--sumaq-radius.1875remDefault border radius (buttons, inputs, wrapper)
--sumaq-radius-sm.125remSmall radius (code, mark)
--sumaq-radius-lg.375remLarge radius (dialog box)

Shadow

Token Default Purpose
--sumaq-shadow-dialog0 .25rem 1rem oklch(0 0 0 / .08)Dialog box shadow
--sumaq-focus-ring0 0 0 .125rem oklch(.363 .039 255 / .35)Focus ring on dialog inputs

Safe to Override

These styles are designed to be customized. Change them freely.

Content typography: font families, font sizes, line heights, margins on headings/paragraphs/lists. The heading scale (h1h6 font sizes) is a good starting point.

Blockquote styling: border color, background, padding, cite formatting.

Table styling: cell padding, border color, header background.

Code styling: <code> and <pre> fonts, backgrounds, text colors.

Link styling: color, underline, hover state.

Button and dialog colors: background, text color, border color on .sumaq-btn, .sumaq-btn-primary, .sumaq-dialog__cancel, .sumaq-link-remove. Stick to colors. Dimensions are structural.

Do Not Override

The editor ships with an accessible foundation: roving tabindex toolbar, aria-pressed button states, focus management, dialog focus trapping, screen reader labels. Overriding toolbar or dialog styles risks breaking that. Two categories:

Structural: will break layout

  • .sumaq-editor flexbox (display, flex-direction)
  • .sumaq-toolbar flex and wrapping (display, flex-wrap, gap)
  • .sumaq-content overflow/resize behavior (overflow-y, resize, min-height)
  • .sumaq-dialog centering and overlay (display, align-items, justify-content, height, width)
  • .sumaq-btn dimensions (height, width), icon alignment and touch targets depend on these
  • .sumaq-dialog__box max-width and centering

Accessible: will break a11y

  • .sumaq-visually-hidden: screen reader text. Removing or modifying this class hides content from assistive technology.
  • Focus indicators (outline, box-shadow on :focus-visible), sighted keyboard users depend on these.
  • aria-pressed button states, the visual pressed state (.sumaq-btn[aria-pressed="true"]) must remain distinguishable.
  • Dialog backdrop (.sumaq-dialog::backdrop), traps visual focus. Making it transparent breaks the modal pattern.
  • Escape hint (.sumaq-esc-hint) positioning, it's absolutely positioned relative to the wrapper.

You can restyle colors and borders on any of these. The guidance is: leave the layout and visibility rules alone unless you test the result with keyboard and screen reader.

Static Properties

Sumaq.defaultConfig   // default config object (toolbar, styleOverrides, onChange)
Sumaq.buttonConfig    // button definitions: label, title, command, value/action
Sumaq.icons           // SVG icon strings keyed by button ID
Sumaq.styles          // base CSS template literal

These are assigned after the class definition. You can read them, and technically you can modify them before creating an instance, but buttonConfig and icons are shared across all instances, so changes are global.

defaultConfig is shallow-merged with your instance config ({ ...defaultConfig, ...config }), so passing any config option replaces the default for that key.

Sanitization

Allowlist-based. Every time HTML enters or leaves the editor (setHTML(), getHTML(), source mode toggle), it passes through sanitization.

What survives: elements in ALLOWED_ELEMENTS (31 semantic tags: p, h1h6, blockquote, pre, cite, ul, ol, li, table, thead, tbody, tr, td, th, hr, strong, em, u, s, sup, sub, mark, code, a, br, img) and attributes in ALLOWED_ATTRS (per-element: href/target/rel on links, src/alt on images, colspan/rowspan on table cells, start on ordered lists, data-placeholder on cite, id globally).

What gets stripped: <script>, <style>, <iframe>, <video>, <audio>, <svg>, <math>, form elements, removed with their content. Unknown elements get unwrapped (children kept). All on* event handlers and javascript: URIs are removed. Inline style and class attributes are handled by a separate cleanup step.

Extensions

The editor supports custom extensions for buttons, dialogs, and formatting that aren't part of the core. Extensions register buttons, declare protected CSS classes, and receive lifecycle hooks (init, onCommand, destroy).

Full documentation: Extensions