monaco-review
    Preparing search index...

    monaco-review

    monaco-review

    Build Status Coverage Status npm

    Code review extension for the Monaco editor — inline comments, threaded replies, and edit history rendered directly in the editor.

    screenshot

    • Add, reply to, edit and remove comments on any line
    • Threaded replies with indentation
    • Full edit history per comment (event-sourced — see Persistence)
    • Navigate between comments (Ctrl/Cmd+F12 next, Ctrl/Cmd+F11 previous)
    • Comment markers in the scrollbar / overview ruler
    • Read-only mode
    • Bring-your-own rich text/markdown rendering, sanitized with DOMPurify
    • Programmatic API: getComments(), selectComment(id), onActiveCommentChanged
    • Customisable rendering, styles (BEM class names), and date formatting
    • Keyboard-accessible toolbar (real <button> elements)
    • Follows the active Monaco theme (light/dark)

    GitHub and GitLab already have excellent review tools — but Monaco gets embedded in many kinds of applications. This library gives you a lightweight way to let users annotate documents rendered in any Monaco instance: internal tools, data pipelines, education platforms, document workflows.

    npm install monaco-review
    

    monaco-editor (>= 0.34) is a peer dependency. The library works with both ESM-imported monaco (Vite, webpack, etc.) and the classic AMD loader — no window.monaco global is required. The only runtime dependency is dompurify (used to sanitize rich-text comment rendering).

    The package ships dual ESM/CJS builds with TypeScript declarations. Alternatively, load the prebuilt IIFE bundle directly in a page — it exposes a MonacoEditorCodeReview global:

    <script src="node_modules/monaco-review/dist/index.global.js"></script>
    
    import { createReviewManager } from "monaco-review";

    const editor = monaco.editor.create(document.getElementById("container"), {
    value: "function add(a, b) {\n return a + b;\n}",
    language: "javascript",
    });

    // Events previously captured via onChange - or [] to start fresh
    const existingEvents = [
    {
    type: "create",
    id: "1",
    createdBy: "developer-1",
    createdAt: Date.now(),
    lineNumber: 2,
    text: "Should we validate the inputs?",
    },
    ];

    const reviewManager = createReviewManager(
    editor,
    "name-of-current-user",
    existingEvents,
    (updatedEvents) => {
    // Called on every change - persist these to your backend
    console.log("events", updatedEvents);
    },
    /* config */ {},
    );

    Comments are stored as an append-only list of events (create / edit / delete). The onChange callback hands you the full event list after every change — serialize it as-is and replay it later via createReviewManager(...) or reviewManager.load(events). Edit history and threading fall out of the event log for free.

    To compute the current comment state from events outside the editor (e.g. server-side), use the exported reducer:

    import { reduceComments } from "monaco-review";

    const store = reduceComments(events);
    // store.comments is a Record<commentId, { comment, history }>

    All fields of ReviewManagerConfig are optional:

    Option Default Description
    readOnly false Disable adding/editing comments (can also toggle via setReadOnlyMode)
    renderText plain text (text) => string | HTMLElement - plug in a markdown renderer (see below)
    onActiveCommentChanged - (comment | undefined) => void - fires when the selected comment changes
    formatDate ISO string (dt: Date | string) => string used when rendering timestamps
    renderComment built-in renderer Fully replace comment rendering: (isActive, item) => HTMLElement
    styles defaultStyles Override inline styles per element class
    setClassNames true Also set CSS class names on rendered elements, for external styling
    editButtonAddText / editButtonRemoveText "Reply" / "Remove" Toolbar button labels
    editButtonEnableRemove true Show the remove button
    showInRuler true Show comment markers in the overview ruler
    commentIndent / commentIndentOffset 20 / 20 Pixel indentation per reply depth
    verticalOffset 0 Vertical pixel adjustment for widgets
    keybindings see below Override keybindings, e.g. { addComment: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F10] }
    Shortcut Action
    Ctrl/Cmd + F10 Add comment at the current line
    Ctrl/Cmd + F12 Next comment
    Ctrl/Cmd + F11 Previous comment
    Ctrl + Enter Save comment (while editing)
    Escape Cancel (while editing)

    All shortcuts (except Ctrl+Enter/Escape inside the textarea) can be overridden via config.keybindings.

    Comment text renders as plain text by default. To render markdown (or any rich text), plug in your renderer via renderText. When it returns a string, the library sanitizes it with DOMPurify before injecting; when it returns an HTMLElement, it is appended as-is (sanitization is then your responsibility):

    import { marked } from "marked";

    createReviewManager(editor, user, events, onChange, {
    renderText: (text) => marked.parse(text, { async: false }),
    });

    Drive the review UI from your own components (e.g. a comment sidebar):

    reviewManager.getComments(); // ReviewComment[] - current state computed from events
    reviewManager.selectComment(id); // activate + scroll into view (undefined clears)
    reviewManager.navigateToComment(direction); // keyboard next/prev equivalent

    createReviewManager(editor, user, events, onChange, {
    onActiveCommentChanged: (comment) => sidebar.highlight(comment?.id),
    });

    Rendered elements carry BEM class names (monaco-review-comment, monaco-review-comment__author, monaco-review-comment--active, monaco-review-toolbar__add, monaco-review-editor__text, ...) so you can theme everything from a stylesheet. The inline defaults can be overridden per class via config.styles, or disable class names entirely with setClassNames: false.

    ReviewManager attaches widgets, view zones, decorations, actions and event listeners to the editor. When the editor or hosting component is unmounted, release them:

    reviewManager.dispose();
    

    For example in React:

    useEffect(() => {
      const rm = createReviewManager(editor, user, events, onChange);
      return () => rm.dispose();
    }, [editor]);
    
    npm install
    npm start # vite dev server - demo at http://localhost:5173/examples/index.html
    npm test # vitest with coverage
    npm run lint # eslint
    npm run typecheck # tsc --noEmit
    npm run build # tsup - builds dist (esm + cjs + iife + d.ts)
    npm run build:demo # vite build - builds the demo site (deployed to GitHub Pages by CI)

    MIT

    FOSSA Status