A modern, extensible markdown editor and previewer for the web.
Installation β’ Quick Start β’ Usage β’ Features β’ API β’ License
Draftly is a powerful, pluggable markdown editor and preview toolkit built on top of CodeMirror 6. It provides a seamless "rich text" editing experience while preserving standard markdown syntax. Draftly also includes a static HTML renderer that produces output visually identical to the editor, making it perfect for blogs, documentation sites, and content management systems.
- π Modern Architecture: Built on CodeMirror 6 with incremental Lezer parsing.
- π¨ Rich Editing: WYSIWYG-like experience with full markdown control.
- π Extensible Plugin System: Add custom rendering, keymaps, and syntax.
- πΌοΈ Static Preview: Render markdown to semantic HTML with visual parity.
- π Theming: First-class support for light and dark modes.
- π¦ Modular Exports: Import only what you need (
draftly/editor,draftly/preview,draftly/plugins).
Install the package via your preferred package manager:
# npm
npm install draftly
# yarn
yarn add draftly
# pnpm
pnpm add draftly
# bun
bun add draftlyDraftly requires the following CodeMirror packages as peer dependencies. Make sure they are installed in your project:
npm install @codemirror/commands @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/viewGet up and running in seconds.
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { draftly } from "draftly";
const view = new EditorView({
state: EditorState.create({
doc: "# Hello, Draftly!",
extensions: [draftly()],
}),
parent: document.getElementById("editor")!,
});Draftly is designed for flexibility. Use it as a CodeMirror extension for interactive editing or as a standalone renderer for static previews.
Here's a complete example using @uiw/react-codemirror:
import CodeMirror from "@uiw/react-codemirror";
import { draftly, ThemeEnum } from "draftly";
import { createAllPlugins } from "draftly/plugins/all";
import { githubDark } from "@uiw/codemirror-theme-github";
function MarkdownEditor() {
return (
<CodeMirror
value="# Welcome to Draftly\n\nStart writing..."
height="500px"
extensions={[
draftly({
theme: ThemeEnum.DARK,
themeStyle: githubDark,
plugins: createAllPlugins(),
lineWrapping: true,
history: true,
indentWithTab: true,
onNodesChange: (nodes) => console.log("AST:", nodes),
}),
]}
/>
);
}| Option | Type | Default | Description |
|---|---|---|---|
theme |
ThemeEnum |
ThemeEnum.AUTO |
Theme mode: LIGHT, DARK, or AUTO. |
themeStyle |
Extension |
undefined |
CodeMirror theme extension (e.g., githubDark). |
plugins |
DraftlyPlugin[] |
[] |
Plugins to enable for rendering and parsing. |
baseStyles |
boolean |
true |
Load default base styles. |
disableViewPlugin |
boolean |
false |
Disable rich rendering (raw markdown mode). |
defaultKeybindings |
boolean |
true |
Enable default CodeMirror keybindings. |
history |
boolean |
true |
Enable undo/redo history. |
indentWithTab |
boolean |
true |
Use Tab for indentation. |
highlightActiveLine |
boolean |
true |
Highlight the current line (in raw mode). |
lineWrapping |
boolean |
true |
Enable line wrapping. |
onNodesChange |
(nodes: DraftlyNode[]) => void |
undefined |
Callback fired on every document update with parsed AST. |
markdown |
MarkdownConfig[] |
[] |
Additional Lezer markdown parser extensions. |
extensions |
Extension[] |
[] |
Additional CodeMirror extensions. |
keymap |
KeyBinding[] |
[] |
Additional keybindings. |
Render markdown to semantic HTML for server-side rendering, static site generation, or read-only views.
import { preview, generateCSS, ThemeEnum } from "draftly";
import { createAllPlugins } from "draftly/plugins/all";
const plugins = createAllPlugins();
const markdown = `
# Hello World
This is a **bold** statement with some \`inline code\`.
- Item 1
- Item 2
- Item 3
`;
// Generate HTML
const html = preview(markdown, {
theme: ThemeEnum.LIGHT,
plugins,
sanitize: true,
wrapperClass: "prose",
});
// Generate matching CSS
const css = generateCSS({
theme: ThemeEnum.LIGHT,
plugins,
wrapperClass: "prose",
includeBase: true,
});
// Use in your app
function ArticlePreview() {
return (
<>
<style>{css}</style>
<article dangerouslySetInnerHTML={{ __html: html }} />
</>
);
}| Option | Type | Default | Description |
|---|---|---|---|
plugins |
DraftlyPlugin[] |
[] |
Plugins for rendering. |
theme |
ThemeEnum |
ThemeEnum.AUTO |
Theme mode. |
sanitize |
boolean |
true |
Sanitize HTML output. Browser only β see below. |
sanitizer |
(html) => string |
undefined |
Sanitizer to use instead of the bundled DOMPurify. Required for SSR. |
wrapperClass |
string |
"draftly-preview" |
CSS class for the wrapper element. |
wrapperTag |
string |
"article" |
HTML tag for the wrapper element. |
markdown |
MarkdownConfig[] |
[] |
Additional parser extensions. |
Warning
sanitize: true does nothing outside a browser. It is implemented with DOMPurify,
which needs a DOM, so during SSR or static generation the option is a no-op and any HTML
in the markdown is emitted unsanitized. Draftly warns on the console when this
happens, but if you render untrusted markdown on a server you must pass your own
sanitizer:
import DOMPurify from "isomorphic-dompurify";
preview(markdown, {
plugins,
sanitizer: (html) => DOMPurify.sanitize(html),
});Draftly does not bundle jsdom β it is heavy, and every browser consumer would pay for
it. Sanitizing at the application layer works equally well.
Draftly's ViewPlugin decorates the editor to hide markdown syntax and render styled content inline. This provides a WYSIWYG-like experience while keeping the source as plain markdown.
- Inline Formatting: Bold, italic, strikethrough, and code are styled in-place.
- Headings: Rendered with proper sizes and weights.
- Lists: Ordered and unordered lists with custom bullets.
- Images: Displayed inline with alt text and captions.
- Links: Clickable with visual distinction.
- Code Blocks: Syntax highlighted with language detection.
Every feature in Draftly is a plugin. Plugins can provide:
- CodeMirror Extensions: Custom decorations, widgets, and behaviors.
- Markdown Parser Extensions: Extend the Lezer parser for custom syntax.
- Keymaps: Add keyboard shortcuts.
- Themes: Inject custom styles based on the current theme.
- Preview Renderers: Define how elements are rendered to static HTML.
import { DraftlyPlugin } from "draftly/editor";
class MyCustomPlugin extends DraftlyPlugin {
name = "my-custom-plugin";
onRegister(context) {
console.log("Plugin registered!", context.config);
}
getExtensions() {
return [
/* CodeMirror extensions */
];
}
getKeymap() {
return [
/* KeyBinding[] */
];
}
getMarkdownConfig() {
return {
/* MarkdownConfig */
};
}
theme(mode) {
return {
/* Theme spec */
};
}
}Access the parsed document structure via the onNodesChange callback. Perfect for building:
- Table of Contents
- Document Outlines
- Navigation Breadcrumbs
- Word/Line Counters
type DraftlyNode = {
from: number; // Start position
to: number; // End position
name: string; // Node type (e.g., "Heading", "Paragraph")
children: DraftlyNode[];
isSelected: boolean; // True if cursor is within this node
};Draftly provides seamless theming with automatic light/dark mode support:
- Auto Detection: Follows system preference with
ThemeEnum.AUTO. - Manual Control: Force
ThemeEnum.LIGHTorThemeEnum.DARK. - Custom Themes: Pass any CodeMirror theme via
themeStyle. - Preview Parity: CSS generation ensures preview matches editor styling.
Import only what you need to minimize bundle size:
// Core package β the editor, the preview renderer, and the light plugins
import { draftly, preview } from "draftly";
// Editor only
import { draftly, DraftlyPlugin } from "draftly/editor";
// Preview only
import { preview, generateCSS } from "draftly/preview";
// Individual plugins
import { HeadingPlugin, ListPlugin } from "draftly/plugins";Three plugins carry large third-party dependencies and live behind their own entry points,
so that nothing importing draftly/plugins pays for them:
| Entry point | Plugin | Dependency | Approx. bundled cost |
|---|---|---|---|
draftly/plugins/mermaid |
MermaidPlugin |
mermaid |
5.3 MB |
draftly/plugins/math |
MathPlugin |
katex |
475 KB |
draftly/plugins/emoji |
EmojiPlugin |
node-emoji |
312 KB |
Compose the set you actually want:
import { draftly } from "draftly";
import { createEssentialPlugins } from "draftly/plugins";
import { MathPlugin } from "draftly/plugins/math";
const extensions = draftly({
plugins: [...createEssentialPlugins(), new MathPlugin()],
});Or take everything from draftly/plugins/all, which pulls all three by design:
import { createAllPlugins } from "draftly/plugins/all";
const extensions = draftly({ plugins: createAllPlugins() });| Export | Path | Description |
|---|---|---|
draftly |
draftly/editor |
Main editor extension factory. |
DraftlyPlugin |
draftly/editor |
Base class for creating plugins. |
ThemeEnum |
draftly/editor |
Enum for theme modes (AUTO, LIGHT, DARK). |
DraftlyNode |
draftly/editor |
Type for AST nodes. |
preview |
draftly/preview |
Function to render markdown to HTML. |
generateCSS |
draftly/preview |
Function to generate CSS for preview styling. |
createEssentialPlugins() |
draftly/plugins |
Builds a fresh set of the essential plugins. Call once per editor. |
createAllPlugins() |
draftly/plugins/all |
Builds a fresh set of every built-in plugin, heavy ones included. Call once per editor. |
MermaidPlugin |
draftly/plugins/mermaid |
Mermaid diagrams. Opt-in β pulls mermaid. |
MathPlugin |
draftly/plugins/math |
LaTeX via KaTeX. Opt-in β pulls katex. |
EmojiPlugin |
draftly/plugins/emoji |
:shortcode: emoji. Opt-in β pulls node-emoji. |
essentialPlugins |
draftly/plugins |
Deprecated β shared array. Use createEssentialPlugins(). |
allPlugins |
draftly/plugins/all |
Deprecated β shared array. Use createAllPlugins(). |
Contributed by the built-in plugins. Mod is Cmd on macOS and Ctrl elsewhere.
| Shortcut | Action |
|---|---|
Mod-B / Mod-I / Mod-Shift-S |
Bold / italic / strikethrough |
Mod-, / Mod-. |
Subscript / superscript |
Mod-Shift-H |
Highlight |
Mod-E / Mod-Shift-E |
Inline code / fenced code block |
Mod-K |
Link |
Mod-Shift-I |
Image |
Mod-Shift-8 / Mod-Shift-7 |
Bullet list / ordered list |
Mod-Shift-9 |
Task list |
Mod-Enter |
Toggle the task(s) on the selected lines |
Mod-Shift-T |
Insert table |
Mod-Alt-Down / Mod-Alt-Right |
Add table row / column |
Mod-Alt-Backspace / Mod-Alt-Delete |
Remove table row / column |
Tab / Shift-Tab (in a table) |
Next / previous cell |
Shift-Enter (in a table) |
Insert a line break inside a cell |
Mod-Enter is the only way to toggle a task without a mouse: the rendered checkbox is
deliberately not focusable, because focusable children inside a contenteditable surface
interfere with the editor's own focus and selection handling.
Draftly supports all modern browsers:
| Browser | Version |
|---|---|
| Chrome | 88+ |
| Firefox | 78+ |
| Safari | 14+ |
| Edge | 88+ |
Table column alignment in the raw markdown uses Intl.Segmenter (Chrome 87+,
Safari 14.1+, Firefox 125+) to group grapheme clusters. Where it is unavailable Draftly
falls back to per-code-point measurement, which still handles CJK, emoji and combining
marks and only slightly over-estimates emoji built from ZWJ sequences. The support floor
above is unchanged, and the rendered table view is unaffected either way β it is laid out
with CSS, not with padding.
Contributions are welcome! Please read our Contributing Guide before submitting a pull request.
MIT Β© NeuroNexul