A WYSIWYG HTML editor component with support for visual and code editing modes, form integration, and extensive formatting capabilities.
A fully-featured editor with all controls. Copy and paste this example to get started quickly:
<k-html-editor name='content'>
<!-- Text formatting -->
<k-control-group slot='toolbar-top-left'>
<kc-bold></kc-bold>
<kc-italic></kc-italic>
<kc-underline></kc-underline>
<kc-strikethrough></kc-strikethrough>
</k-control-group>
<kc-inline-code slot='toolbar-top-left'></kc-inline-code>
<kc-menu slot='toolbar-top-left'>
<k-icon slot='icon' src='...'></k-icon>
<span slot='label'>Text Style</span>
<kc-format-block tag='p'>Paragraph</kc-format-block>
<kc-format-block tag='h1'>Heading 1</kc-format-block>
<kc-format-block tag='h2'>Heading 2</kc-format-block>
<kc-format-block tag='h3'>Heading 3</kc-format-block>
<kc-code-block></kc-code-block>
</kc-menu>
<!-- Lists, alignment, colors, link, table -->
<k-control-group slot='toolbar-top-left'>
<kc-bullet-list></kc-bullet-list>
<kc-number-list></kc-number-list>
</k-control-group>
<k-control-group slot='toolbar-top-left'>
<kc-align-left></kc-align-left>
<kc-align-center></kc-align-center>
<kc-align-right></kc-align-right>
<kc-align-justify></kc-align-justify>
</k-control-group>
<k-control-group slot='toolbar-top-left'>
<kc-text-color></kc-text-color>
<kc-text-background-color></kc-text-background-color>
</k-control-group>
<kc-clear-formatting slot='toolbar-top-left'></kc-clear-formatting>
<k-control-group slot='toolbar-top-left'>
<kc-create-link></kc-create-link>
<kc-insert-table></kc-insert-table>
</k-control-group>
<!-- Mode toggle + format code -->
<kc-mode slot='toolbar-top-right'></kc-mode>
<kc-format-code slot='toolbar-top-right'></kc-format-code>
<kc-fullscreen slot='toolbar-top-right'></kc-fullscreen>
<!-- Bottom toolbar stats -->
<kc-word-count slot='toolbar-bottom-left'></kc-word-count>
<kc-character-count slot='toolbar-bottom-left'></kc-character-count>
</k-html-editor>
When the editor's value contains HTML that Lexical does not support (such as <svg>,
<script>, <style>, <video>, etc.), it automatically
defaults to code mode to preserve the content. If the user attempts to switch to visual mode, a confirmation
dialog warns that the incompatible code will be lost.
<k-html-editor id=svgEditor value="...">
<kc-mode slot='toolbar-top-right'></kc-mode>
</k-html-editor>
Custom nodes extend what Lexical understands in visual mode. Import a node file and list its name in the
nodes attribute (comma-separated filenames without .js). The built-in
HtmlComment node preserves HTML comments as inline visual chips rather than forcing code mode —
comments round-trip through the editor without being stripped.
<!-- Import the HtmlComment node -->
<script type='module' src='src/components/htmlEditorNodes/HtmlComment.js'></script>
<!-- Register the node with nodes attribute -->
<k-html-editor nodes='HtmlComment' value='<p>This paragraph has an <!-- important note --> inline comment.</p>'>
<kc-mode slot='toolbar-top-right'></kc-mode>
</k-html-editor>
The controls attribute lets you opt into a built-in toolbar without writing any slot markup. Set it
directly on <k-html-editor> to one of the four levels below.
The four toolbar slots — toolbar-top-left, toolbar-top-right,
toolbar-bottom-left, toolbar-bottom-right — all have slot fallback content driven by
the controls value. Providing your own child element for any of these slots replaces just that
section while leaving the others intact.
Every available control: text formatting, inline code, text-style dropdown (p, h1–h3, blockquote, code block), lists, alignment (with justify), text and background colours, clear formatting, link, table insertion, mode toggle, and word/character count.
<k-html-editor controls="full"></k-html-editor>
A balanced everyday toolbar: bold, italic, underline, strikethrough, inline code, text-style dropdown, lists, alignment, link, and mode toggle. Word count shown in the bottom bar.
<k-html-editor controls="normal"></k-html-editor>
Essential controls only: bold, italic, underline, bullet list, numbered list, and mode toggle. Ideal for simple content forms.
<k-html-editor controls="minimal"></k-html-editor>
All controls levels share the full JavaScript API — getValue(),
setValue(), bold(), etc. You can still target any individual section by slotting a child
into the relevant named slot.
Slots available for custom content:
toolbar-top-left, toolbar-top-right,
toolbar-bottom-left, toolbar-bottom-right.
The default height is 400px. Override it with an inline style or CSS class on the element:
<k-html-editor style='height: 500px'></k-html-editor>
The disabled attribute makes the editor non-interactive (Lexical is set to non-editable in visual mode, Monaco is set to readOnly in code mode), mutes the toolbar, fades the host, and excludes the field from form submission. The readonly attribute lets the user select and copy text and switch modes, but they can't edit; the value is still submitted with the form.
<k-html-editor disabled value="..."></k-html-editor>
<k-html-editor readonly value="..."></k-html-editor>
Use the editor without controls and interact with it programmatically using custom buttons and the JavaScript API:
<!-- Custom toolbar -->
<div>
<button onclick="myEditor.bold()">Bold</button>
<button onclick="myEditor.italic()">Italic</button>
<button onclick="myEditor.unorderedList()">List</button>
<button onclick="myEditor.setTextColor('#ff0000')">Red</button>
<button onclick="insertHTML()">Insert</button>
<button onclick="getContent()">Get Value</button>
</div>
<!-- Editor without controls -->
<k-html-editor id="myEditor"></k-html-editor>
<script>
const myEditor = document.getElementById('myEditor');
// Set initial content
myEditor.setValue('<p>Select text and use the buttons above!</p>');
function insertHTML() {
myEditor.insertHTML('<p><b>Inserted content!</b></p>');
}
function getContent() {
alert(myEditor.getValue());
}
</script>
new HtmlEditor()
Extends ShadowComponent which extends LitElement.
name: StringThe name attribute for form submission.
value: StringThe HTML content of the editor. Can be get or set via JavaScript or HTML attribute.
mode: StringCurrent editing mode. Either 'visual' (WYSIWYG) or 'code' (raw HTML). Default:
'visual'. If the initial value contains content that is incompatible with the visual
editor (such as <svg>, <script>, <style>, etc.), the
editor automatically switches to 'code' mode to preserve the content. Custom nodes can extend what is
considered compatible by defining a static isVisualCompatible(domNode) method.
controls: StringActivates a built-in toolbar. Accepted values:
'none' (default) — no built-in toolbar; use named slots to build your own'minimal' — bold, italic, underline, lists, mode toggle'normal' — adds strikethrough, inline code, text-style dropdown, alignment, link, word count'full' — every available control including colours, clear formatting, table insertion, and
character countThe built-in controls appear as slot fallback content in the six toolbar slots. Providing your own child with a matching slot name for any section replaces just that section's defaults.
nodes: StringComma-separated list of custom Lexical node module filenames (without .js) to load from the
htmlEditorNodes/ directory. Custom nodes can define two static hooks:
isVisualCompatible(domNode) — returns true if the node handles a DOM construct that
would otherwise be considered incompatible (e.g., HTML comments)preprocessHtml(htmlString) — transforms raw HTML before Lexical parses it (e.g., converting
<!-- --> to a parseable <span>)
lexical-src: StringBase URL for loading Lexical modules on this specific editor instance. When set, this overrides both the global
window.kempo.lexicalUrl config and the built-in CDN default. See Global
Configuration for the full resolution order.
monaco-src: StringBase URL for loading the Monaco editor on this specific editor instance. When set, this overrides both the global
window.kempo.monacoUrl config and the built-in CDN default. See Global
Configuration for the full resolution order.
disabled: BooleanWhen true, the editor is non-interactive (Lexical's setEditable(false) in visual mode, Monaco's readOnly in code mode), the toolbar is muted, the host fades to opacity: 0.6, and the field is excluded from form submission. Syncs to disabled attribute.
readonly: BooleanWhen true, the user can select and copy text but not edit. The value is still submitted with the form. Syncs to readonly attribute.
required: BooleanWhen true and the editor is empty (no non-whitespace text), the element reports a valueMissing validity error to its parent <form>. Syncs to required attribute.
The editor resolves dependency URLs in the following priority order (highest to lowest):
lexical-src / monaco-src on the element itself
window.kempo.lexicalUrl / window.kempo.monacoUrl
https://esm.sh (Lexical) and
https://cdn.jsdelivr.net/npm/monaco-editor@…/min (Monaco)
Set the global config before any <k-html-editor> initialises to apply a custom URL site-wide.
This is useful when you have installed the packages locally via npm install lexical or
npm install monaco-editor and serve them yourself:
<script>
window.kempo = window.kempo || {};
// Point to your locally served Lexical modules (must be an esm.sh-compatible base URL)
window.kempo.lexicalUrl = '/node_modules';
// Point to your locally served Monaco build
window.kempo.monacoUrl = '/node_modules/monaco-editor/min';
</script>
The per-element attributes take precedence over the global config, so you can mix strategies — e.g., use a local
Lexical build globally while one specific editor still points to a CDN via its lexical-src attribute.
setMode(mode): thisSet the editing mode. Accepts 'visual' or 'code'. If switching to 'visual'
and the current content is incompatible, a confirmation dialog warns that incompatible code will be lost.
toggleMode(): thisToggle between visual and code modes. Same incompatibility warning applies when switching to visual mode.
getValue(): StringReturns the current HTML content.
setValue(html): thisSets the HTML content of the editor. If the editor is in visual mode and the new content is incompatible, automatically switches to code mode.
clear(): thisClears all content from the editor.
bold(): thisApplies bold formatting to the selected text.
italic(): thisApplies italic formatting to the selected text.
underline(): thisApplies underline formatting to the selected text.
strikethrough(): thisApplies strikethrough formatting to the selected text.
orderedList(): thisCreates or toggles an ordered (numbered) list.
unorderedList(): thisCreates or toggles an unordered (bulleted) list.
alignLeft(): thisAligns text to the left.
alignCenter(): thisCenters the text.
alignRight(): thisAligns text to the right.
alignJustify(): thisJustifies the text.
setTextColor(color): thisSets the foreground color of selected text. Accepts any valid CSS color value (hex, rgb, color name).
removeTextColor(): thisRemoves foreground color from selected text.
setTextBackgroundColor(color): thisSets the background color of selected text. Accepts any valid CSS color value (hex, rgb, color name).
removeTextBackgroundColor(): thisRemoves background color from selected text.
removeFormat(): thisRemoves all formatting from the selected text.
formatBlock(tag): thisFormats the current block with the specified HTML tag (e.g., 'h1', 'h2', 'h3', 'p', 'pre').
insertHTML(html): thisInserts HTML at the current cursor position.
insertAtCursor(html): thisAlias for insertHTML().
insertTable(rows, columns, includeHeaders, cellData): thisInserts an HTML table at the cursor position.
rows - Number of rows to createcolumns - Number of columns to createincludeHeaders - Boolean indicating whether to include header row (default: false)cellData - Optional 2D array of cell content. If provided, rows/columns are determined from array
dimensionsinsertElementAtCursor(element, selectAfter): thisInserts a DOM element at the cursor position or replaces selection.
element - DOM element to insertselectAfter - Boolean indicating whether to select the inserted element after insertion (default:
true)replaceSelectionWithElement(element, selectAfter): thisReplaces the current selection with a DOM element.
element - DOM element to insertselectAfter - Boolean indicating whether to select the inserted element after replacement
(default: true)wrapSelection(before, after, savedSelection): thisWraps the selected text with HTML tags. Handles complex cases like unwrapping existing tags and preventing nested pre tags.
before - Opening HTML tag(s) to wrap withafter - Closing HTML tag(s) to wrap withsavedSelection - Optional previously saved selection text to use instead of current selection
getSelection(): Object | nullReturns an object containing information about the current selection:
{ text, html, range, selection }
getSelectedText(): StringReturns the plain text of the current selection.
getSelectedHTML(): StringReturns the HTML of the current selection.
setSelection(startNode, startOffset, endNode, endOffset): thisProgrammatically sets the selection range.
selectAll(): thisSelects all content in the editor.
replaceSelection(html): thisReplaces the current selection with the provided HTML.
deleteSelection(): thisDeletes the currently selected content.
getValueWithSelectionMarkers(): ObjectReturns the editor content with special Unicode markers indicating cursor position or selection boundaries. Useful for preserving selection state during content transformations.
Returns object with properties:
html - HTML content with markers insertedhasCursor - Boolean indicating if cursor position is markedhasSelection - Boolean indicating if selection is markedcursorMarker - Unicode character used for cursor (\uFFF0)selectionStart - Unicode character for selection start (\uFFF1)selectionEnd - Unicode character for selection end (\uFFF2)selectedText - Plain text of the selectionsetValueFromSelectionMarkers(html, markers): thisSets editor content and restores cursor/selection from Unicode markers. Companion method to
getValueWithSelectionMarkers().
html - HTML content containing marker charactersmarkers - Object with marker information (hasCursor, hasSelection, cursorMarker, selectionStart,
selectionEnd)createLink(url): thisCreates a hyperlink from the selected text.
unlink(): thisRemoves hyperlink from the selected text.
insertImage(url): thisInserts an image at the cursor position.
undo(): thisUndoes the last action. Works in both visual mode (Lexical) and code mode (Monaco).
redo(): thisRedoes the last undone action. Works in both visual mode (Lexical) and code mode (Monaco).
These methods control the Monaco code editor and are available in code mode. When used with
controls="full", the toolbar exposes buttons for these automatically.
copyToClipboard(): thisCopies the full editor content to the clipboard.
setEditorTheme(theme): thisSets the Monaco editor theme. Accepts 'auto', 'light', or 'dark'.
openFind(): thisOpens the Monaco Find & Replace widget.
foldAll(): thisFolds all collapsible regions in the code editor.
unfoldAll(): thisUnfolds all collapsed regions in the code editor.
increaseFontSize(): thisIncreases the code editor font size by 2px (max 40px).
decreaseFontSize(): thisDecreases the code editor font size by 2px (min 8px).
setWordWrap(enabled): thisEnables or disables word wrapping in the code editor.
setMinimap(enabled): thisShows or hides the minimap in the code editor.
resolveMonacoTheme(): StringReturns the resolved Monaco theme string ('vs' or 'vs-dark') based on the current
editorTheme setting.
changeDispatched when the content changes. Event detail: { value: String }
mode-changedDispatched when the editing mode changes between 'visual' and 'code'. Event detail: { mode: String }