MicroText
Rich text editor wrapper around Tiptap for the Saaslivery platform. Provides a toolbar, output syncing to hidden inputs, and a consistent editing experience across apps.
JS: public/js/microtext.js (ES module)
CSS: public/js/microtext.css
Version: 0.1.0
Quick start
Include the CSS and the JS module, then initialize:
<link rel="stylesheet" href="/public/js/microtext.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
<form method="post" action="/api/notes">
<div id="editor"></div>
<input type="hidden" name="body" id="body-input">
<button type="submit">Save</button>
</form>
<script type="module">
import Microtext from '/public/js/microtext.js';
Microtext.init({
element: '#editor',
content: '<p>Hello world</p>',
placeholder: 'Start writing...',
output: '#body-input',
format: 'html',
toolbar: true,
});
</script>
Note
MicroText loads Tiptap extensions from esm.sh at runtime — no build step needed. The <script> tag must have type="module".
Microtext.init(options)
Returns a Tiptap Editor instance (or null if the element is not found).
Options
| Option | Type | Default | Description |
|---|---|---|---|
element |
string or HTMLElement |
required | CSS selector or DOM node where the editor mounts. Works with Alpine's $el. |
content |
string |
'' |
Initial HTML content. |
placeholder |
string |
'Start writing...' |
Placeholder text shown when the editor is empty. |
output |
string or HTMLElement |
null |
Hidden input that receives the editor's output on every change. |
format |
string |
'html' |
Output format written to the output element: 'html' or 'json'. |
toolbar |
boolean |
true |
Show the formatting toolbar. |
borderless |
boolean |
false |
Remove the border from the editor wrapper. |
editable |
boolean |
true |
false renders read-only content (no toolbar, no border). |
onUpdate |
function |
null |
(html, json) => {} — called on every content change. |
Toolbar buttons
The toolbar is organized into groups separated by thin dividers:
| Group | Buttons |
|---|---|
| Formatting | Bold, Italic, Underline, Strikethrough |
| Headings | Heading 2, Heading 3 |
| Lists | Bullet list, Ordered list, Task list |
| Blocks | Blockquote, Code block, Inline code |
| Insert | Horizontal rule, Link |
Buttons highlight when the cursor is inside matching content (e.g., Bold highlights when text is bold).
Link button
Clicking the link button opens a prompt() dialog. Enter a URL to set a link, clear the field to remove the link.
Output syncing
When output is set, MicroText automatically writes the editor content to the target element's value on every change. This lets you use a standard hidden <input> inside a <form>:
<div id="editor"></div>
<input type="hidden" name="description" id="desc-input">
Microtext.init({
element: '#editor',
output: '#desc-input',
format: 'html', // or 'json' for Tiptap JSON
});
On form submit, desc-input contains the current HTML (or JSON).
Read-only mode
Render saved content without editing:
Microtext.init({
element: '#content-display',
content: savedHtml,
editable: false,
});
This removes the toolbar and border, producing clean rendered output.
Using with Alpine.js
<div x-data="{ editor: null }" x-init="
import('/public/js/microtext.js').then(m => {
editor = m.default.init({
element: $refs.editor,
content: '',
placeholder: 'Write a comment...',
output: $refs.hidden,
toolbar: true,
});
})
">
<div x-ref="editor"></div>
<input type="hidden" x-ref="hidden" name="comment_body">
</div>
Destroying the editor
The returned editor instance has a destroy_microtext() method that cleans up the Tiptap editor and unwraps the DOM:
var editor = Microtext.init({ element: '#editor' });
// Later, when removing the editor:
editor.destroy_microtext();
Tiptap extensions included
MicroText bundles these Tiptap extensions:
| Extension | What it adds |
|---|---|
| StarterKit | Paragraphs, headings (H2–H4), bold, italic, strike, code, code block, blockquote, bullet list, ordered list, horizontal rule, hard break. |
| Underline | Underline formatting. |
| Link | Clickable links with rel="noopener noreferrer nofollow". |
| Placeholder | Placeholder text when the editor is empty. |
| TaskList + TaskItem | Checkbox task lists (nested supported). |
CSS classes
All classes are prefixed with mt-.
| Class | Element | Purpose |
|---|---|---|
.mt-wrap |
div |
Outer wrapper with border and rounded corners. |
.mt-wrap:focus-within |
.mt-wrap |
Highlights border with --brand color on focus. |
.mt-readonly |
.mt-wrap |
Removes border for read-only mode. |
.mt-borderless |
.mt-wrap |
Removes border when borderless: true. |
.mt-toolbar |
div |
Formatting toolbar container. |
.mt-toolbar-btn |
button |
Individual toolbar button (28x28px). .active when format is active. |
.mt-toolbar-sep |
span |
Thin vertical separator between button groups. |
.mt-editor |
div |
Tiptap content area. |
.mt-link |
a |
Styled link inside editor content. |
CSS custom properties
MicroText reads these CSS variables:
| Variable | Used for |
|---|---|
--bs-border-color |
Wrapper and toolbar borders |
--surface |
Wrapper and toolbar background |
--bg |
Toolbar background |
--bg-sunken |
Code block and inline code background |
--brand |
Focus border, active toolbar button, blockquote accent, links |
--brand-subtle |
Active toolbar button background |
--text-primary |
Editor text, headings, bold |
--text-secondary |
Toolbar button color |
--text-muted |
Placeholder, strikethrough, muted text |
Example: task description field
<div class="form-group">
<label class="form-label">Description</label>
<div id="task-desc-editor"></div>
<input type="hidden" name="description" id="task-desc-hidden">
</div>
<script type="module">
import Microtext from '/public/js/microtext.js';
Microtext.init({
element: '#task-desc-editor',
content: '{{ task.description | e }}',
placeholder: 'Add a description...',
output: '#task-desc-hidden',
format: 'html',
onUpdate: function(html) {
// auto-save via HTMX
htmx.ajax('PATCH', '/api/task/{{ task.id }}', {
headers: { 'Content-Type': 'application/json' },
values: JSON.stringify({ description: html })
});
}
});
</script>