README.md

August 21, 2026 · View on GitHub

Draftly

A modern, extensible markdown editor and previewer for the web.

npm version npm downloads license GitHub stars TypeScript CodeMirror 6

InstallationQuick StartUsageFeaturesAPILicense


Overview

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.

Why Draftly?

  • 🚀 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).

Installation

Install the package via your preferred package manager:

# npm
npm install draftly

# yarn
yarn add draftly

# pnpm
pnpm add draftly

# bun
bun add draftly

Peer Dependencies

Draftly 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/view

Quick Start

Get 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")!,
});

Usage

Draftly is designed for flexibility. Use it as a CodeMirror extension for interactive editing or as a standalone renderer for static previews.

Editor Integration

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),
        }),
      ]}
    />
  );
}

Editor Configuration (DraftlyConfig)

OptionTypeDefaultDescription
themeThemeEnumThemeEnum.AUTOTheme mode: LIGHT, DARK, or AUTO.
themeStyleExtensionundefinedCodeMirror theme extension (e.g., githubDark).
pluginsDraftlyPlugin[][]Plugins to enable for rendering and parsing.
baseStylesbooleantrueLoad default base styles.
disableViewPluginbooleanfalseDisable rich rendering (raw markdown mode).
defaultKeybindingsbooleantrueEnable default CodeMirror keybindings.
historybooleantrueEnable undo/redo history.
indentWithTabbooleantrueUse Tab for indentation.
highlightActiveLinebooleantrueHighlight the current line (in raw mode).
lineWrappingbooleantrueEnable line wrapping.
onNodesChange(nodes: DraftlyNode[]) => voidundefinedCallback fired on every document update with parsed AST.
markdownMarkdownConfig[][]Additional Lezer markdown parser extensions.
extensionsExtension[][]Additional CodeMirror extensions.
keymapKeyBinding[][]Additional keybindings.

Static Preview

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 }} />
    </>
  );
}

Preview Configuration (PreviewConfig)

OptionTypeDefaultDescription
pluginsDraftlyPlugin[][]Plugins for rendering.
themeThemeEnumThemeEnum.AUTOTheme mode.
sanitizebooleantrueSanitize HTML output. Browser only — see below.
sanitizer(html) => stringundefinedSanitizer to use instead of the bundled DOMPurify. Required for SSR.
wrapperClassstring"draftly-preview"CSS class for the wrapper element.
wrapperTagstring"article"HTML tag for the wrapper element.
markdownMarkdownConfig[][]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.


Features

🎯 Rich Text Editing

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.

🔌 Plugin Architecture

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 */
    };
  }
}

🌲 AST Access

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
};

🌗 Theming

Draftly provides seamless theming with automatic light/dark mode support:

  • Auto Detection: Follows system preference with ThemeEnum.AUTO.
  • Manual Control: Force ThemeEnum.LIGHT or ThemeEnum.DARK.
  • Custom Themes: Pass any CodeMirror theme via themeStyle.
  • Preview Parity: CSS generation ensures preview matches editor styling.

📦 Modular Imports

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";

The heavy plugins are opt-in

Three plugins carry large third-party dependencies and live behind their own entry points, so that nothing importing draftly/plugins pays for them:

Entry pointPluginDependencyApprox. bundled cost
draftly/plugins/mermaidMermaidPluginmermaid5.3 MB
draftly/plugins/mathMathPluginkatex (peer)0 — you install it
draftly/plugins/emojiEmojiPluginnode-emoji312 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()],
});
KaTeX is a peer dependency, and its CSS is yours to bring

katex is a peer dependency, marked optional — install it yourself if you use MathPlugin, and skip it otherwise. It is not bundled into dist/, so you get exactly one copy of it.

Rendered math is unstyled without KaTeX's stylesheet, and Draftly does not inject it by default. If you have a build step, import it — this is the cheap path, and your bundler handles the fonts:

import "katex/dist/katex.min.css";

If you have no build step — a <script> tag, a CDN, an embedded editor — let the plugin inject the stylesheet instead:

new MathPlugin({ injectStyles: true });

That injects KaTeX's CSS with all 20 font faces inlined as data: URIs, once per document, and costs about 360 KB. The fonts are inlined rather than referenced because KaTeX's own @font-face rules use relative fonts/KaTeX_* paths, which a <style> element resolves against your page URL rather than the package — so they 404 unless you happen to serve them from there. The stylesheet sits behind a dynamic import() and ships as its own chunk, so leaving injectStyles at its default of false costs nothing.

Or take everything from draftly/plugins/all, which pulls all three by design:

import { createAllPlugins } from "draftly/plugins/all";

const extensions = draftly({ plugins: createAllPlugins() });

API Reference

Exports

ExportPathDescription
draftlydraftly/editorMain editor extension factory.
DraftlyPlugindraftly/editorBase class for creating plugins.
ThemeEnumdraftly/editorEnum for theme modes (AUTO, LIGHT, DARK).
DraftlyNodedraftly/editorType for AST nodes.
previewdraftly/previewFunction to render markdown to HTML.
generateCSSdraftly/previewFunction to generate CSS for preview styling.
createEssentialPlugins()draftly/pluginsBuilds a fresh set of the essential plugins. Call once per editor.
createAllPlugins()draftly/plugins/allBuilds a fresh set of every built-in plugin, heavy ones included. Call once per editor.
MermaidPlugindraftly/plugins/mermaidMermaid diagrams. Opt-in — pulls mermaid.
MathPlugindraftly/plugins/mathLaTeX via KaTeX. Opt-in — katex is an optional peer dependency.
EmojiPlugindraftly/plugins/emoji:shortcode: emoji. Opt-in — pulls node-emoji.

Keyboard Shortcuts

Contributed by the built-in plugins. Mod is Cmd on macOS and Ctrl elsewhere.

ShortcutAction
Mod-B / Mod-I / Mod-Shift-SBold / italic / strikethrough
Mod-, / Mod-.Subscript / superscript
Mod-Shift-HHighlight
Mod-E / Mod-Shift-EInline code / fenced code block
Mod-KLink
Mod-Shift-IImage
Mod-Shift-8 / Mod-Shift-7Bullet list / ordered list
Mod-Shift-9Task list
Mod-EnterToggle the task(s) on the selected lines
Mod-Shift-TInsert table
Mod-Alt-Down / Mod-Alt-RightAdd table row / column
Mod-Alt-Backspace / Mod-Alt-DeleteRemove 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.


Browser Support

Draftly supports all modern browsers:

BrowserVersion
Chrome88+
Firefox78+
Safari14+
Edge88+

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.


Contributing

Contributions are welcome! Please read our Contributing Guide before submitting a pull request.


License

MIT © NeuroNexul