Markdown Editor: A WYSIWYG and Markup Editor Built on Gravity UI

TL;DR

Hi! My name is Sergey Makhnatkin, and I work as a developer in the User Experience team at Yandex Cloud. Last year, we wrote about our Gravity UI design system and component library. Since then, the system has been updated multiple times and expanded with new features, and today I want to talk about a new tool — Markdown Editor — which significantly simplifies working with documentation.

We’ll talk about the history of building the user interface, architectural features, and technical details of integration and developing custom extensions — and, of course, why all of this is available as open source.

By the way, you can try the tool here:

Why we need our own Markdown Editor

To make it convenient to store and structure corporate information, we built the Wiki platform, which allows creating knowledge bases. Alongside the knowledge base, we developed documentation approaches such as Docs as Code, where documentation and code live side by side in a file repository (.md files). This is how the Diplodoc platform emerged.

What Wiki and Diplodoc have in common is that both platforms work with a markdown dialect — Yandex Flavored Markdown (YFM) — which is used at Nebius, Bitrix, DoubleCloud, Mappable, and Meteum.

Over time, we noticed there are two groups of users who think about creating and editing text differently. Some prefer to see the final result right away, working with text like in MS Word, Confluence, or Notion. Others trust only markup and prefer to format pages using markdown. We couldn’t find any well-known libraries that work in both WYSIWYG and markdown modes at the same time. For example, Notion is WYSIWYG only, while code editors usually provide only markdown and preview mode.

We developed a markdown editor that can work in two modes simultaneously: a visual mode (WYSIWYG) and a markup mode (markdown). In the first mode, toolbar icons help format the text; in the second, users can manually edit the markdown source. In addition, our solution stores the document as an .md file regardless of which mode was used to create it.

This is what the visual editor looks like, where you can format text using buttons:

Full screen image

And this is the markup mode, where formatting elements are indicated using special characters:

Full screen image

Markdown Editor capabilities in Gravity UI

The editor complies with the CommonMark standard and supports both standard markdown and YFM. We also added the ability to extend the syntax with other markdown dialects, such as GitHub Flavored Markdown. At the same time, the editor lets you switch from markup mode to WYSIWYG mode, while the document itself is stored as md markup or extended md (for example, in the case of YFM).

Extensions

The editor comes with many built-in extensions and settings. For example, Mermaid diagrams and HTML blocks:

Full screen image
Full screen image

We tried to make the editor core easy to extend. Developers can create their own extension or add extra functionality to:

  • add new entities — blocks or text modifiers;
  • further configure the markdown parser;
  • add actions that allow controlling the editor from the outside;
  • enrich UI functionality — for example, show a list of available commands when typing a slash;
  • modify current behavior — for example, insert images and files and upload them to storage.

Here are some examples of such extensions we built for our Wiki:

  • collaborative editing mode;
Full screen image
Full screen image
Full screen image
Full screen image
  • includes;
Full screen image
  • section structure;
Full screen image
  • sections for creating a convenient grid;
Full screen image
  • markdown mode with preview;
Full screen image
  • and many more.
Full screen image

Markup can be transformed automatically. If you prefer working without a mouse, the visual editor mode supports special characters that let you apply markup directly in the text. For example, ** turns text bold in WYSIWYG mode. With these characters, you can format text and create inline and block code.

You can also open the extensions menu by typing /.

Full screen image

Presets

The editor lets you configure the toolbar for each project individually, but it ships with a set of ready-made configurations — presets.

The editor without presets:

Full screen image

The CommonMark preset provides support for standard markdown elements: bold, italic, headings, lists, links, quotes, and code blocks.

Full screen image

In the default preset, you also get strikethrough text, plus a table where only text can be placed in cells. This preset corresponds to standard markdown-it.

Full screen image

As mentioned above, the editor also supports YFM, so it integrates great with Diplodoc. In the YFM preset, additional elements appear: advanced tables, file and image insertion, checkboxes, cut, tabs, and monospace font.

Full screen image

The full preset includes even more elements.

Full screen image

Architecture

The editor’s WYSIWYG mode is built on the well-known ProseMirror library, while markup mode uses CodeMirror. ProseMirror supports editing with formatting, whereas CodeMirror is better suited for situations where you need to work with raw text.

We chose these libraries because they were created by the same author, are consistent in architecture and implementation approaches, are backed by a large community, are used in many editors, and are well optimized for working with text. For example, the transaction system for applying changes to the document, view decorations, DOM virtualization, and support for the syntax of many programming languages.

Integration

Our editor is easy to connect as a React hook:


import React from 'react';
import {useMarkdownEditor, MarkdownEditorView} from '@gravity-ui/markdown-editor';
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';

function Editor({onSubmit}) {
  const editor = useMarkdownEditor({allowHTML: false});

  React.useEffect(() => {
    function submitHandler() {
      // Serialize current content to markdown markup
      const value = editor.getValue();
      onSubmit(value);
    }

    editor.on('submit', submitHandler);
    return () => {
      editor.off('submit', submitHandler);
    };
  }, [onSubmit]);

  return <MarkdownEditorView stickyToolbar autofocus toaster={toaster} editor={editor} />;
}

We use components from the GravityUI uikit library. This ensures that the entire interface is consistent and follows unified style guidelines. Using these components also provides a high degree of consistency and recognizability for users, making the editor even more convenient to work with.

We have detailed instructions on how to connect the editor to a React app, as well as how to connect various extensions, such as YandexGPT, Mermaid or LaTeX.

Integrating custom extensions

A number of additional extensions are already integrated into the editor. But if that’s not enough, developers can add their own extensions to the editor’s WYSIWYG mode. We discussed what extensions can enable above.

If you want to add a new block or text modifier, you first need to configure the internal markdown-it instance using the configureMd method. Next, you should add knowledge about the new entity using the addNode or addMark methods, passing the entity name and a callback function that returns an object with three required fields:

import insPlugin from 'markdown-it-ins';
export const underlineMarkName = 'ins';

export const UnderlineSpecs: ExtensionAuto = (builder) => {
    builder
        .configureMd((md) => md.use(insPlugin))
        .addMark(underlineMarkName, () => ({
            spec: {
                parseDOM: [{tag: 'ins'}, {tag: 'u'}],
                toDOM() {
                    return ['ins'];
                },
            },
            toMd: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true},
            fromMd: {tokenSpec: {name: underlineMarkName, type: 'mark'}},
        }));
};
  • spec — the ProseMirror specification;
  • fromMd — configuration for parsing markdown markup into ProseMirror’s internal representation;
  • toMd — configuration for serializing the entity into markdown markup.

For example, below is the configuration of an extension for underlined text. It can be extended by adding an action using the addAction method:

import {toggleMark} from 'prosemirror-commands';

const undAction = 'underline';

builder
    .addAction(undAction, ({schema}) => ({
        isActive: (state) => Boolean(isMarkActive(state, markType)),
        isEnable: toggleMark(underlineType(schema)),
        run: toggleMark(underlineType(schema)),
      })
  )

Such an action can be called in code as follows:

// editor – an editor instance obtained as a result of calling useMarkdownEditor
editor.actions.underline.run(),

In the documentation, you can find the full guide on creating a new extension.


We’re constantly expanding the horizons of how our editor can be used: right now we’re working on a plugin for VS Code, which will allow working with .md files in a convenient WYSIWYG mode прямо from within the editor. We also plan to add a fully featured mobile mode. This will enable every user to work in our editor with only a mobile phone at hand.

Our editor didn’t appear overnight: it’s the result of accumulated experience and knowledge. We’re proud that it is fully based on open-source products, including the reliable and proven tools ProseMirror, CodeMirror, and markdown-it, as well as our own developments — Diplodoc and Gravity UI.

You can always contribute to the editor’s development: create a pull request or help resolve the current issues listed in the Issues section. Your support and fresh perspective will help us make the editor better. And if you find our project useful, please star our GitHub repository — it really matters :)

Markdown Editor: A WYSIWYG and Markup Editor Built on Gravity UI

Sign in to save this post