@mydrift/etch
v0.5.0
Published
A clean, fully-featured WYSIWYG editor built on Tiptap v3
Maintainers
Readme
Install
bun add @mydrift/etchnpm install @mydrift/etchpnpm add @mydrift/etchyarn add @mydrift/etchUsage
import { EtchEditor } from "@mydrift/etch/react";
import "@mydrift/etch/styles";
export default function App() {
return (
<EtchEditor
content="<p>Hello, Etch.</p>"
onUpload={async (file) => {
// upload to your CDN, return the public URL
return "https://example.com/uploaded.png";
}}
/>
);
}SSR (Next.js App Router, Remix)
Etch defaults to immediatelyRender={false} and is safe to render directly in
React Server Component trees as long as the component itself is a Client
Component. In Next.js App Router, mark the file with "use client" (or load
the editor with next/dynamic and { ssr: false } if you want to skip
hydration entirely).
Props
| Prop | Type | Default | What |
| --- | --- | --- | --- |
| content | string | "" | Initial HTML (or Tiptap JSON) |
| onUpdate | (editor) => void | — | Fires on every content change |
| onStats | ({chars, words}) => void | — | Live character / word count callback |
| onSubmit | (editor) => void | — | Fires on the submit keybinding (see submitOn) |
| submitOn | "enter" \| "mod-enter" | "mod-enter" | "enter" → Enter submits, Shift+Enter inserts hard break. "mod-enter" → Cmd/Ctrl+Enter submits, Enter newlines. |
| onUpload | (file) => Promise<string> | — | File upload handler; required for media slash commands |
| theme | "light" \| "dark" \| "system" | "system" | Scope theme to the editor wrapper (sets data-theme) |
| placeholder | string | "Type '/' for commands…" | Empty-line placeholder text |
| density | "comfortable" \| "compact" | "comfortable" | "compact" collapses min-height to 2.5rem and padding to 0.5rem for chat-style composers |
| editable | boolean | true | Disable to render read-only |
| slashItems | SlashCommandItem[] | built-in | Override the slash command palette |
| extensions | Extension[] | [] | Additional Tiptap extensions to merge in |
| className | string | — | Applied to the outermost wrapper |
| immediatelyRender | boolean | false | Tiptap's hydration knob; leave false for SSR safety |
| disableX (12 flags) | boolean | false | Drop entire extensions — see "Disabling extensions" below |
| nodeAttrs | { [node]: string[] } | — | Roundtrip custom HTML attrs on built-in nodes |
| mentions | MentionConfig | — | Enable @-autocomplete — see "Mentions" below |
| mobileToolbar | boolean \| "auto" | "auto" | Floating "+" trigger on coarse-pointer devices |
Sizing can also be tuned directly with CSS variables — --etch-min-height and
--etch-padding on .etch-editor-wrapper and .ProseMirror respectively.
Imperative ref
const ref = useRef<EtchEditorHandle>(null);
// …
ref.current?.getHTML(); // "<p>…</p>"
ref.current?.getText(); // plain text (Tiptap's editor.getText())
ref.current?.getMarkdown(); // "# heading\n…" (requires tiptap-markdown, bundled)
ref.current?.getEmailSafeHTML(); // HTML rewritten for email clients (see below)
ref.current?.getCharCount(); // number
ref.current?.getWordCount(); // number
ref.current?.editor; // raw Tiptap Editor instanceChat-style composer
<EtchEditor
density="compact"
placeholder="Write a comment…"
submitOn="enter"
onSubmit={(editor) => {
sendComment(editor.getHTML());
editor.commands.clearContent();
}}
/>Mentions
<EtchEditor
mentions={{
items: async (query) => searchUsers(query),
// optional: custom row render
renderItem: ({ item }) => <UserRow user={item.data as User} />,
// optional: override how the chip is inserted
onInsert: ({ item, editor, range }) => {
editor.chain().focus().deleteRange(range).insertContent({
type: "mention",
attrs: { id: item.id, label: item.label },
}).run();
},
}}
/>Triggered by @ by default — override via mentions.char. The popup auto-positions, supports arrow/Enter/Tab keyboard nav, and respects the editor's theme.
Disabling extensions
Twelve flags drop entire extensions (slash menu and input-rule shortcuts):
<EtchEditor
disableCodeBlock
disableTaskList
disableMath
disableCallouts
disableToggleHeadings
disableFileEmbeds
/>Available: disableCodeBlock, disableHorizontalRule, disableTaskList, disableMath, disableCallouts, disableToggleHeadings, disableFileEmbeds (video / audio / file attachment / file paste-drop), disableImage, disableTable, disableYoutube, disableTypography, disableTableOfContents.
Custom node attributes
Roundtrip app-side IDs (or any data-*) through built-in nodes:
<EtchEditor
nodeAttrs={{
image: ["data-fs-node-id"],
fileAttachment: ["data-fs-node-id", "data-file-version"],
}}
/>
// later: editor.getHTML() preserves the attrsRead-only viewer
For rendering saved Etch HTML outside an editor (feed rows, archived comments):
import { EtchViewer } from "@mydrift/etch/react";
<EtchViewer
html={comment.body_html}
theme="dark"
density="compact"
// same disable / nodeAttrs / theme / density props as EtchEditor
/>Renders identical to <EtchEditor editable={false}> minus the editor chrome (no slash menu, bubble menu, drag handle, upload pipeline).
Email-safe HTML
const html = ref.current?.getEmailSafeHTML();
// or as a static utility:
import { toEmailSafeHTML } from "@mydrift/etch";
const html = toEmailSafeHTML(editor.getHTML());- Strips Etch-internal
data-*attributes - Inlines styles on
blockquote,code,pre,th,td - Rewrites callouts → blockquote with bold first line
- Rewrites toggle headings → plain headings
- Rewrites file-attachment embeds →
<a href>links
Requires DOMParser (browser, or a DOM-shimmed Node environment).
Mobile
A floating "+" trigger appears on coarse-pointer devices (phones, tablets); tapping it inserts a / so the slash menu opens above the on-screen keyboard. Toggle via mobileToolbar={true | false | "auto"} (default "auto").
Features
- Slash commands — Type
/for a grouped command palette with 25+ block types - Rich formatting — Bold, italic, links, colors, highlights via bubble menu
- Code blocks — 55 languages with syntax highlighting
- Drag & drop — Reorder any block by the left-side handle
- Media uploads — Images (with resize), videos, audio, file attachments with upload progress
- Callout blocks — 8 styled callout types with SVG icons
- Markdown paste/copy — Auto-converts pasted markdown to rich text
- Tables, task lists, math — All the usual suspects, no plugin fishing
Entrypoints
| Path | What |
| --- | --- |
| @mydrift/etch | Framework-agnostic Tiptap extensions |
| @mydrift/etch/react | React components (<EtchEditor>, hooks) |
| @mydrift/etch/styles | CSS (also exposed as dist/etch.css) |
Development
bun install # Install deps
bun run build # Build with tsup (ESM + CJS + .d.ts)
bun run dev # Watch mode
bun run typecheck # TypeScript check
cd demo && bun install # Run the demo locally
bun run devLicense
MIT
