Framework
Milkdown/milkdown avatar
Milkdown/milkdown

Milkdown editor: the plugin framework it puts over ProseMirror and remark

🍼 Plugin driven WYSIWYG markdown editor framework.

11,972 stars554 forksTypeScriptMIT

At a glance

What is it?
Milkdown is a plugin-driven WYSIWYG markdown editor framework in TypeScript, built on ProseMirror for the editing surface and remark for markdown. The repository README is links and credits, so the setup path, the default plugin set and browser support all sit outside it.
Who is it for?
Milkdown suits a TypeScript team that wants a Typora style WYSIWYG surface with markdown as the stored format and is willing to assemble the editor from plugins. It does not suit a project that needs a documented default feature set, a browser support statement or a server rendering story on day one.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The plugin framework is the layer between ProseMirror's document and remark's markdown

Milkdown is a plugin-driven WYSIWYG markdown editor framework, written in TypeScript and MIT licensed. It is built on top of ProseMirror, which supplies the document model and the editing surface, and remark, which handles markdown. The Typora comparison in the README is about the editing feel rather than the internals: what you see is the rendered document, and what you keep is markdown.

That split is the design. A WYSIWYG surface needs ProseMirror's state and transactions, a markdown editor needs a parser and a serializer, and something has to keep the two in step. Milkdown is that something, and it makes the joining part replaceable. Plugins are the unit of composition, which is also the word the repository description leads with.

The alternative approach is to skip the framework and use ProseMirror and remark directly. Then the plugin wiring, the markdown round trip and the editor chrome are yours to write and to keep working, and you start with no default feature set. Milkdown supplies that layer, and the cost of the shortcut is that the framework's own release cycle decides when your editor changes behaviour underneath you.

There is no install line in the README, only a link to milkdown.dev

The README does not contain an installation command. It points at the official documentation website, https://milkdown.dev/, and moves on. The npm badges in that README resolve to the scoped package `@milkdown/core`, which tells you the naming pattern to expect: the pieces arrive as `@milkdown/...` packages rather than one package called milkdown.

Three things the README leaves unsaid, and each one blocks a decision before a line of code is written. It names no Node or browser requirement, so compatibility cannot be checked from the repository. It does not say which plugins ship by default, so there is no way to tell from it whether a table or a footnote works out of the box. It lists no peer dependencies, so a lockfile decision has nothing to be made from.

The plugin driven shape reaches into the install as well. A framework you assemble piece by piece means a set of packages you choose, and the README gives no recipe for which set. Closing that gap is what the documentation site is for, which makes the README a signpost rather than a starting point.

The root package.json is @milkdown/monorepo, version 0.0.0 and private

The manifest at the repository root is not the thing you install. It is named `@milkdown/monorepo`, its version is `0.0.0`, and it carries `"private": true`. The installable code sits under `packages/`, and the workspace is described by `pnpm-workspace.yaml` next to `pnpm-lock.yaml`.

That layout has a consequence for anyone arriving from a package search. Reading the root manifest tells you nothing about what you would get from npm, because the root version is pinned at `0.0.0` and never moves. The version numbers that matter are on the releases: `v7.22.2` on 2026-09-23, `v7.22.1` on 2026-08-12 and `v7.22.0` on 2026-08-03.

The root also carries the files a monorepo needs and an application does not: `.changeset/`, `.husky/`, `.nvmrc`, `tsconfig.base.json`, `knip.json`, a `storybook/` folder and an `e2e/` folder. Reading the repository means reading a contributor's view of the project, which is a different artifact from the one a consumer installs.

Releases are cut by changeset, then published across the workspace

Publishing runs from scripts in the root manifest rather than from a manual tag. `ci:version` runs `changeset version && pnpm install --no-frozen-lockfile`, turning pending changeset entries into version bumps and refreshing the lockfile. `release` runs `changeset publish`, and `ci:publish` builds first, then runs `pnpm publish --access public -r --no-git-checks --tag latest` across the workspace.

The `-r` flag is what makes this a multi-package release, and it is the part to notice as a consumer. Several packages can move in one operation, so the repository tag alone does not tell you which `@milkdown/...` version you would resolve. Pin the packages that matter to you rather than tracking the repository release.

The cadence is regular, three releases inside eight weeks in that record, and the last push landed on 2026-10-01 with the repository not archived. Planned work is not in the code, though. It lives on a GitHub project board the README calls the Milkdown TODO and on the repository milestones page, so something you need can be on the plan without carrying a date.

A build runs tsc project references, then a per-package build

Two build steps run in sequence. `build:tsc` invokes `tsc -b tsconfig.json --verbose` across the project references, and `build:post` runs `pnpm -r run build` so each package produces its own output. The `build` script chains the two.

The tsconfig files are not all written by hand. `codegen` runs `tsx ./scripts/gen-ts-config.mts`, a script that generates the per-package tsconfig entries, while `tsconfig.base.json` holds the shared compiler settings. Add a package to the workspace and the generated configuration follows, which is convenient until you want to know why a compiler setting has the value it has.

Unused output gets measured too. `knip` is a script and `knip.json` sits at the root, so unused exports and dead files are something the project checks for rather than something a reviewer discovers. None of this appears in the README, and `CONTRIBUTING.md` is the only pointer to how a change is expected to be made.

oxlint gates the build with deny-warnings, and commits run through commitlint

The test entry point chains two checks: `test` runs `pnpm test:lint && pnpm test:unit`. The lint half is `oxlint -c .oxlintrc.json --deny-warnings`, so a warning fails the same as an error. The unit half is `vitest run`, configured through `vitest.config.mts`, with a watch variant and an end-to-end suite reached through `pnpm --filter=@milkdown/e2e test`.

Formatting and commit shape are enforced, by different tools from that linter. `format` runs `lint-staged`, configured in `.lintstagedrc.json`, `fix` runs `oxfmt .`, and `commit` runs `git-cz`. The `.husky/` directory holds the hooks that the `prepare` script installs. `commitlint.config.js` at the root, with `@commitlint/cli` and `@commitlint/config-conventional` in devDependencies, keeps messages in conventional form.

For an outside contributor this toolchain is not optional. Cloning the repository and editing a plugin is cheap. Getting a change past the project's own gates means having pnpm, oxlint, oxfmt, vitest, changesets and commitlint behave the way the maintainers have them, which is a steeper entry than a README this short suggests.

Browser support, the default plugin set and the roadmap are all off the README

What Milkdown cannot tell you from the repository matters as much as what it can. There is no browser support statement, so you cannot decide whether it fits the environments your users actually have. There is no statement about server rendering either, and a ProseMirror based editor assumes a DOM, so that question has to be answered before you commit to it. Accessibility goes unmentioned too, which for a component meant to replace a plain textarea is a real gap rather than a detail.

The plugin model cuts both ways. Because features are plugins, nothing in the README commits to which of them exist. A table, a math block, a diagram, an image uploader: each is something you add, and each is something you verify for yourself. The repository does hold `docs/`, `dev/`, `storybook/` and `e2e/`, so worked examples are present, but they are folders inside a monorepo rather than a list of capabilities anyone maintains as a specification.

Security and conduct have dedicated files, `SECURITY.md` and `CODE_OF_CONDUCT.md`, and the README points at a Discord community and at the contribution guide for the rest. The thanks section names JetBrains Open Source, Vercel OSS, Cypress, Nx and Algolia as supporters. Everything a new user needs in order to choose the framework is somewhere other than the README.

Editorial conclusion

Milkdown suits a TypeScript team that wants a Typora style WYSIWYG surface with markdown as the stored format and is willing to assemble the editor from plugins. It does not suit a project that needs a documented default feature set, a browser support statement or a server rendering story on day one. Before adopting, read the setup path at milkdown.dev and check which `@milkdown/...` packages exist, because the repository README documents none of that.

Frequently asked questions

What is the difference between Milkdown and Lexical?

Milkdown is a plugin-driven WYSIWYG markdown editor framework built on ProseMirror and remark, so markdown is the format it reads and writes. Lexical is an editor framework that works from its own document model rather than markdown, so a markdown round trip is not part of its default shape.

What is a milkdown alternative?

ProseMirror and remark are what Milkdown is built on, so using them directly is the alternative approach. You then write the plugin wiring and the markdown round trip yourself and begin with no default feature set, while Milkdown supplies that layer as a framework.

How do I install Milkdown?

The README contains no installation command and points at the official documentation website instead. Its npm badges resolve to `@milkdown/core`, so the packages are published under the `@milkdown` scope and the repository root is a private workspace named `@milkdown/monorepo`.

Which package manager and test runner does Milkdown use?

pnpm, with `pnpm-workspace.yaml` and `pnpm-lock.yaml` at the repository root. The `test` script runs `pnpm test:lint && pnpm test:unit`, where the lint step is oxlint with `--deny-warnings` and the unit step is `vitest run`.

Does the Milkdown README document the default plugin set?

No. It names no default plugins and no peer dependencies, so there is no way to tell from it which features a plain install includes. The repository also holds `docs/`, `dev/`, `storybook/` and `e2e/` folders rather than a maintained list of capabilities.

Official sources

  1. License: MIT
  2. Milkdown/milkdown on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/milkdown-milkdown.svg)](https://hysenlabs.com/projects/milkdown-milkdown)