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
- JS finds the
<textarea>inside[data-ln-editor] - Creates a
<div contenteditable>editing surface - Hides the textarea (CSS, via
data-ln-editor-source) - On every content change, syncs
surface.innerHTML → textarea.value - 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
| Attribute | On element | Owned by | Description |
|---|---|---|---|
data-ln-editor | Container <div> | ln-editor | Marks the container as an editor instance |
data-ln-editor-action | <button> | ln-editor | Formatting 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:
| Shortcut | Action |
|---|---|
| Ctrl+B | Toggle bold |
| Ctrl+I | Toggle italic |
| Ctrl+U | Toggle underline |
| Ctrl+K | Insert / edit link |
On macOS, use ⌘ instead of Ctrl.
Toolbar actions reference
data-ln-editor-action | Effect | Shortcut |
|---|---|---|
bold | Toggle bold | Ctrl+B |
italic | Toggle italic | Ctrl+I |
underline | Toggle underline | Ctrl+U |
strikethrough | Toggle strikethrough | — |
heading-2 | Toggle H2 | — |
heading-3 | Toggle H3 | — |
heading-4 | Toggle H4 | — |
blockquote | Toggle blockquote | — |
code | Toggle code block | — |
ordered-list | Toggle numbered list | — |
unordered-list | Toggle bullet list | — |
link | Insert/edit link | Ctrl+K |
unlink | Remove link | — |
clear | Remove all formatting | — |
paragraph | Reset 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
| Event | Type | Detail | When |
|---|---|---|---|
ln-editor:changed | Notification | { html, target } | Content changed |
ln-editor:before-change | Lifecycle (cancelable) | { action, target } | Before formatting command |
ln-editor:focus | Notification | { target } | Surface focused |
ln-editor:blur | Notification | { target } | Surface blurred |
ln-editor:set-content | Request | { html } | Set content programmatically |