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 |
|---|---|---|
files | File[] | All dropped files |
imageFiles | File[] | Dropped files with image/* MIME type |
html | string | HTML content from the drop data (if any) |
text | string | Plain text content from the drop data (if any) |
originalEvent | DragEvent | The 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 |
|---|---|---|
files | File[] | 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 |
|---|---|---|
h1 | Heading 1 | <h1> |
h2 | Heading 2 | <h2> |
h3 | Heading 3 | <h3> |
h4 | Heading 4 | <h4> |
h5 | Heading 5 | <h5> |
h6 | Heading 6 | <h6> |
p | Paragraph | <p> |
pre | Code block | <pre> |
blockquote | Block quote | <blockquote> with <cite> |
bold | Bold | <strong> |
italic | Italic | <em> |
underline | Underline | <u> |
strikethrough | Strikethrough | <s> |
superscript | Superscript | <sup> |
subscript | Subscript | <sub> |
mark | Highlight | <mark> |
code | Inline code | <code> |
link | Insert/edit link | <a href>, dialog with three types: URL (with optional target="_blank"), email (mailto: with optional subject), phone (tel:) |
anchor | Insert/edit anchor | <a id>: ID must start with a letter, only letters/numbers/hyphens/underscores allowed. Validated live in the dialog. |
alignLeft | Align left | removes alignment class |
alignCenter | Center | .text-center |
alignRight | Align right | .text-right |
ul | Unordered list | <ul> |
ol | Ordered list | <ol> |
hr | Horizontal rule | <hr> |
table | Table | <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. |
image | Image | <img> (dialog for src, alt) |
undo | Undo | browser undo |
redo | Redo | browser redo |
source | Source view | toggles <textarea> with raw HTML. Switching back to rich text sanitizes the content. |
'|' | Separator | visual 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-bg | oklch(.985 .003 107) | Editor wrapper background |
--sumaq-bg-secondary | transparent | Toolbar, table header background |
--sumaq-bg-tertiary | oklch(.947 .007 81) | Escape hint background |
--sumaq-bg-active | oklch(.947 .007 81) | Button active state |
--sumaq-bg-content | oklch(.985 .003 107) | Content area background |
--sumaq-bg-content-focus | oklch(.985 .003 107) | Content area on focus |
--sumaq-bg-code | oklch(.96 .005 95) | Inline <code> background |
--sumaq-bg-pre | oklch(.2 .01 255) | <pre> and source view background |
--sumaq-bg-mark | oklch(.94 .035 90) | <mark> highlight background |
--sumaq-bg-dialog | oklch(.985 .003 107) | Dialog box background |
--sumaq-bg-backdrop | oklch(0 0 0 / .35) | Dialog backdrop overlay |
Text
| Token | Default | Purpose |
|---|---|---|
--sumaq-text | oklch(.423 .003 107) | Primary text color |
--sumaq-text-secondary | oklch(.55 .003 107) | Secondary text (blockquote, cite) |
--sumaq-text-tertiary | oklch(.7 .003 107) | Tertiary text (placeholders) |
--sumaq-text-code | oklch(.45 .04 255) | Inline <code> text color |
--sumaq-text-pre | oklch(.9 .005 255) | <pre> and source view text color |
Accent
| Token | Default | Purpose |
|---|---|---|
--sumaq-accent | oklch(.308 .051 253) | Links, caret, primary buttons, active states |
--sumaq-accent-hover | oklch(.25 .05 253) | Accent hover state |
--sumaq-accent-text | oklch(.985 .003 107) | Text on accent-colored backgrounds |
Danger
| Token | Default | Purpose |
|---|---|---|
--sumaq-danger | oklch(.5 .13 30) | Delete/remove buttons |
--sumaq-danger-hover | oklch(.43 .13 30) | Danger hover state |
Border
| Token | Default | Purpose |
|---|---|---|
--sumaq-border | oklch(.925 .008 92) | Editor wrapper border, dialog inputs |
--sumaq-border-hover | oklch(.363 .039 255) | Border hover state |
--sumaq-border-subtle | oklch(.947 .007 81) | Dialog header/footer dividers |
--sumaq-border-table | oklch(.925 .008 92) | Table cell borders, horizontal rule |
--sumaq-border-blockquote | oklch(.308 .051 253) | Blockquote left border |
Radius
| Token | Default | Purpose |
|---|---|---|
--sumaq-radius | .1875rem | Default border radius (buttons, inputs, wrapper) |
--sumaq-radius-sm | .125rem | Small radius (code, mark) |
--sumaq-radius-lg | .375rem | Large radius (dialog box) |
Shadow
| Token | Default | Purpose |
|---|---|---|
--sumaq-shadow-dialog | 0 .25rem 1rem oklch(0 0 0 / .08) | Dialog box shadow |
--sumaq-focus-ring | 0 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 (h1–h6 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-editorflexbox (display,flex-direction).sumaq-toolbarflex and wrapping (display,flex-wrap,gap).sumaq-contentoverflow/resize behavior (overflow-y,resize,min-height).sumaq-dialogcentering and overlay (display,align-items,justify-content,height,width).sumaq-btndimensions (height,width), icon alignment and touch targets depend on these.sumaq-dialog__boxmax-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-shadowon:focus-visible), sighted keyboard users depend on these. aria-pressedbutton 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, h1–h6, 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