Editor

Mode

Skin

Theme

Overview & Philosophy

ln-editor is applied to a container via <div data-ln-editor> wrapping an authored toolbar ([role="toolbar"]) and a standard <textarea name="...">. On initialization, it hides the textarea and injects an accessible <div contenteditable="true" role="textbox"> editing surface seeded with the initial text. Toolbar clicks format selected text, while live input events sanitize content and sync textarea.value = surface.innerHTML, ensuring native form submissions submit current HTML without custom serialize code.

The toolbar is authored HTML, not generated by JS. The consumer controls which formatting buttons appear, their order, and their labels. This follows Mindset §1: "Markup Is the Application." The editing surface uses @include prose so content looks identical to how it will render in a .prose display container.

How it works

  1. JS finds the <textarea> inside [data-ln-editor]
  2. Creates a <div contenteditable> editing surface
  3. Hides the textarea (CSS, via data-ln-editor-source)
  4. On every content change, syncs surface.innerHTML → textarea.value
  5. On form submit, the textarea already has the current HTML — the server receives it

For full documentation see the ln-editor README and the architecture reference.

Anatomy

The minimal markup contract:

<div data-ln-editor>
    <div role="toolbar" aria-label="Text formatting">
        <ul>
            <li><button type="button" data-ln-editor-action="bold" aria-label="Bold">
                <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-bold"></use></svg>
            </button></li>
            <li><button type="button" data-ln-editor-action="italic" aria-label="Italic">
                <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-italic"></use></svg>
            </button></li>
        </ul>
    </div>
    <textarea name="content" placeholder="Write something..."></textarea>
</div>

Toolbar structure

A container with role="toolbar" wraps the toolbar. Each <ul> is a visual button group — multiple <ul> elements create separator lines between groups. <button data-ln-editor-action="..."> binds each formatting action. The consumer decides which actions to include.

Attributes reference

AttributeOn elementOwned byDescription
data-ln-editorContainer <div>ln-editorMarks the container as an editor instance
data-ln-editor-action<button>ln-editorFormatting action to execute on click
data-ln-editor-source<textarea>ln-editor (auto)JS sets this to hide the textarea via CSS. Removed on destroy()

Progressive enhancement

Without JS, the role="toolbar" container is inert and the <textarea> is a normal text input. With JS, the textarea is hidden and the contentEditable surface takes over.

Example — Full toolbar

All available formatting actions. Active buttons are highlighted with the accent color as you move the cursor through formatted content.

Example — Minimal toolbar

Only bold, italic, and link — suitable for comments or short descriptions. The toolbar is authored HTML — include only what you need.

Example — Pre-filled content

Server-rendered HTML in the textarea is used as initial editor content. The editing surface renders it with @include prose styling.

Example — Form integration

The editor syncs content to the hidden <textarea> on every change. Click Save to see the raw HTML that would be submitted.

Keyboard shortcuts

Standard keyboard shortcuts work inside the editing surface:

ShortcutAction
Ctrl+BToggle bold
Ctrl+IToggle italic
Ctrl+UToggle underline
Ctrl+KInsert / edit link

On macOS, use ⌘ instead of Ctrl.

Toolbar actions reference

data-ln-editor-actionEffectShortcut
boldToggle boldCtrl+B
italicToggle italicCtrl+I
underlineToggle underlineCtrl+U
strikethroughToggle strikethrough—
heading-2Toggle H2—
heading-3Toggle H3—
heading-4Toggle H4—
blockquoteToggle blockquote—
codeToggle code block—
ordered-listToggle numbered list—
unordered-listToggle bullet list—
linkInsert/edit linkCtrl+K
unlinkRemove link—
clearRemove all formatting—
paragraphReset to paragraph—

API

const el = document.querySelector('[data-ln-editor]');

// Read current HTML content
el.lnEditor.getHTML();

// Set content programmatically
el.lnEditor.setHTML('<p>New content</p>');

// Or via request event (coordinator pattern)
el.dispatchEvent(new CustomEvent('ln-editor:set-content', {
    detail: { html: '<p>Content via event</p>' }
}));

// Destroy — restores textarea visibility
el.lnEditor.destroy();

Example — Programmatic control

Use the buttons to set content or destroy/recreate the editor instance.

Events reference

EventTypeDetailWhen
ln-editor:changedNotification{ html, target }Content changed
ln-editor:before-changeLifecycle (cancelable){ action, target }Before formatting command
ln-editor:focusNotification{ target }Surface focused
ln-editor:blurNotification{ target }Surface blurred
ln-editor:set-contentRequest{ html }Set content programmatically