Skip to main content

Monaco Editor vs CodeMirror 6 vs Sandpack: In-Browser Code Editors 2026

·PkgPulse Team

Monaco Editor vs CodeMirror 6 vs Sandpack: In-Browser Code Editors 2026

TL;DR

Embedding a code editor in a browser-based application — playgrounds, online IDEs, documentation, admin panels, notebook environments — comes down to three principal options in 2026. Monaco Editor is the VS Code editor engine, open-sourced by Microsoft — it brings the full VS Code experience (IntelliSense, multi-cursor, go-to-definition, full TypeScript language server) in a 2–5MB bundle that works but is heavy for most use cases. CodeMirror 6 is the complete rewrite from 2021 — modular, lightweight (tree-shakeable from ~50kB for a basic editor), highly extensible, excellent accessibility, and used by Firefox DevTools, Replit, and many others; it's the right choice for most custom editor integrations. Sandpack is CodeSandbox's live preview stack — it combines a CodeMirror-based editor with a browser-based Node.js runtime (via Nodebox) to run and preview actual React/JavaScript code without a server; it's purpose-built for interactive code examples and playgrounds. For feature-complete IDE experience with TypeScript IntelliSense: Monaco. For custom editor integration with minimal bundle overhead: CodeMirror 6. For live React/JavaScript playgrounds with code execution: Sandpack.

Key Takeaways

  • Monaco is VS Code — same engine, full IntelliSense, ~5MB gzipped, needs lazy loading
  • CodeMirror 6 is modular — install only what you need, ~50-200kB depending on features
  • Sandpack executes code — runs actual JavaScript/TypeScript/React in the browser via Nodebox
  • Monaco requires a worker — needs MonacoWebpackPlugin or equivalent for service workers
  • CodeMirror 6 has better mobile support — fully keyboard accessible, touch-friendly
  • Sandpack uses CodeMirror — the editor layer is CodeMirror 6, not a standalone editor
  • Monaco supports all VS Code extensions syntax — TextMate grammars for 500+ languages

Use Case Map

Full IDE in browser (TypeScript playground)  → Monaco Editor
Lightweight custom code editor               → CodeMirror 6
Interactive React/JS playground              → Sandpack
Documentation code examples (editable)      → Sandpack or CodeMirror 6
SQL/YAML/JSON editor in admin panel         → CodeMirror 6
Low-bandwidth or mobile-first editor        → CodeMirror 6
Notebook (Jupyter-like) cells               → CodeMirror 6
Code review / diff display                  → Monaco (diff editor) or CodeMirror 6

Monaco Editor: The VS Code Engine

Monaco is VS Code's editor component extracted as a standalone library. If you want TypeScript IntelliSense, hover tooltips, and refactoring tools in the browser, this is the starting point.

Installation

npm install @monaco-editor/react
# Or vanilla (larger, more control):
npm install monaco-editor

React Integration

// The simplest path — @monaco-editor/react
import Editor from "@monaco-editor/react";

export function CodeEditor() {
  return (
    <Editor
      height="400px"
      defaultLanguage="typescript"
      defaultValue={`// TypeScript with full IntelliSense
interface User {
  id: number;
  name: string;
  email: string;
}

function greet(user: User): string {
  return \`Hello, \${user.name}!\`;
}`}
      theme="vs-dark"
      options={{
        minimap: { enabled: false },
        fontSize: 14,
        tabSize: 2,
        wordWrap: "on",
        scrollBeyondLastLine: false,
        automaticLayout: true, // Resize on container resize
      }}
    />
  );
}

Controlled Editor with Change Handlers

import Editor from "@monaco-editor/react";
import { useState, useRef } from "react";
import type { Monaco } from "@monaco-editor/react";
import type { editor } from "monaco-editor";

interface CodeEditorProps {
  initialValue: string;
  language: string;
  onChange?: (value: string) => void;
  readOnly?: boolean;
}

export function CodeEditor({ initialValue, language, onChange, readOnly = false }: CodeEditorProps) {
  const [value, setValue] = useState(initialValue);
  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);

  function handleEditorDidMount(editor: editor.IStandaloneCodeEditor, monaco: Monaco) {
    editorRef.current = editor;

    // Add keyboard shortcut
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
      console.log("Save:", editor.getValue());
    });

    // Focus editor
    editor.focus();
  }

  function handleChange(newValue: string | undefined) {
    const v = newValue ?? "";
    setValue(v);
    onChange?.(v);
  }

  return (
    <Editor
      height="400px"
      language={language}
      value={value}
      onChange={handleChange}
      onMount={handleEditorDidMount}
      theme="vs-dark"
      options={{
        readOnly,
        minimap: { enabled: false },
        fontSize: 14,
        lineNumbers: "on",
        folding: true,
        renderWhitespace: "selection",
      }}
    />
  );
}

TypeScript IntelliSense Configuration

import Editor, { useMonaco } from "@monaco-editor/react";
import { useEffect } from "react";

export function TypeScriptEditor() {
  const monaco = useMonaco();

  useEffect(() => {
    if (!monaco) return;

    // Configure TypeScript compiler options
    monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
      target: monaco.languages.typescript.ScriptTarget.ES2020,
      allowNonTsExtensions: true,
      moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
      module: monaco.languages.typescript.ModuleKind.CommonJS,
      noEmit: true,
      esModuleInterop: true,
      jsx: monaco.languages.typescript.JsxEmit.React,
      reactNamespace: "React",
      allowJs: true,
      typeRoots: ["node_modules/@types"],
    });

    // Add custom type definitions
    const libSource = `
      declare module "my-lib" {
        export function doSomething(input: string): number;
        export interface Config {
          debug: boolean;
          timeout: number;
        }
      }
    `;
    monaco.languages.typescript.typescriptDefaults.addExtraLib(
      libSource,
      "file:///node_modules/my-lib/index.d.ts"
    );

    // Configure diagnostics
    monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
      noSemanticValidation: false,
      noSyntaxValidation: false,
    });
  }, [monaco]);

  return (
    <Editor
      height="400px"
      defaultLanguage="typescript"
      defaultValue={`import { doSomething } from "my-lib";
const result = doSomething("hello"); // IntelliSense works!
`}
      theme="vs-dark"
    />
  );
}

Diff Editor

import { DiffEditor } from "@monaco-editor/react";

export function CodeDiff({ original, modified }: { original: string; modified: string }) {
  return (
    <DiffEditor
      height="400px"
      language="typescript"
      original={original}
      modified={modified}
      theme="vs-dark"
      options={{
        renderSideBySide: true,
        readOnly: true,
        minimap: { enabled: false },
      }}
    />
  );
}

Lazy Loading (Required for Performance)

// next.config.js — exclude monaco from SSR
const nextConfig = {
  webpack: (config) => {
    // Monaco needs workers — use MonacoWebpackPlugin
    return config;
  },
};

// Component — dynamic import to avoid SSR
import dynamic from "next/dynamic";

const Editor = dynamic(
  () => import("@monaco-editor/react"),
  {
    ssr: false,
    loading: () => <div>Loading editor...</div>,
  }
);

CodeMirror 6: Modular and Lightweight

CodeMirror 6 (released 2021) is a complete rewrite with a modular architecture — each feature is a separate package. You compose the editor from extension packages.

Installation

# Core packages
npm install @codemirror/view @codemirror/state

# Language support (pick what you need)
npm install @codemirror/lang-javascript @codemirror/lang-css @codemirror/lang-html
npm install @codemirror/lang-json @codemirror/lang-sql @codemirror/lang-python
npm install @codemirror/lang-markdown

# Themes
npm install @codemirror/theme-one-dark

# Common extensions
npm install @codemirror/commands @codemirror/search @codemirror/fold

# Or use the convenience package (includes everything)
npm install codemirror

Basic Setup (Vanilla JS)

import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";

const view = new EditorView({
  doc: `function hello(name: string) {
  return \`Hello, \${name}!\`;
}`,
  extensions: [
    basicSetup,           // Line numbers, search, history, etc.
    javascript({ typescript: true }),
    oneDark,
    EditorView.lineWrapping,
    EditorView.updateListener.of((update) => {
      if (update.docChanged) {
        console.log("Changed:", update.state.doc.toString());
      }
    }),
  ],
  parent: document.getElementById("editor")!,
});

React Integration (useCodeMirror)

import { useEffect, useRef } from "react";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";
import { keymap } from "@codemirror/view";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";

interface CodeMirrorEditorProps {
  initialValue: string;
  language?: "javascript" | "typescript";
  onChange?: (value: string) => void;
  readOnly?: boolean;
}

export function CodeMirrorEditor({
  initialValue,
  language = "typescript",
  onChange,
  readOnly = false,
}: CodeMirrorEditorProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const view = new EditorView({
      state: EditorState.create({
        doc: initialValue,
        extensions: [
          basicSetup,
          keymap.of([...defaultKeymap, indentWithTab]),
          javascript({ typescript: language === "typescript", jsx: true }),
          oneDark,
          EditorState.readOnly.of(readOnly),
          EditorView.updateListener.of((update) => {
            if (update.docChanged) {
              onChange?.(update.state.doc.toString());
            }
          }),
        ],
      }),
      parent: containerRef.current,
    });

    viewRef.current = view;

    return () => {
      view.destroy();
    };
  }, []); // Only on mount — value changes handled imperatively

  // Update content programmatically (when value changes externally)
  useEffect(() => {
    const view = viewRef.current;
    if (!view) return;

    const current = view.state.doc.toString();
    if (current !== initialValue) {
      view.dispatch({
        changes: {
          from: 0,
          to: view.state.doc.length,
          insert: initialValue,
        },
      });
    }
  }, [initialValue]);

  return <div ref={containerRef} style={{ height: "400px", overflow: "auto" }} />;
}

Custom Extension: Autocomplete

import { autocompletion, CompletionContext, CompletionResult } from "@codemirror/autocomplete";

// Custom completion source
function myCompletions(context: CompletionContext): CompletionResult | null {
  const word = context.matchBefore(/\w*/);
  if (!word || (word.from === word.to && !context.explicit)) return null;

  return {
    from: word.from,
    options: [
      { label: "useEffect", type: "function", info: "React hook for side effects" },
      { label: "useState", type: "function", info: "React hook for state" },
      { label: "useCallback", type: "function", info: "Memoize callback functions" },
      { label: "useMemo", type: "function", info: "Memoize expensive computations" },
    ],
  };
}

// Add to extensions
const extensions = [
  basicSetup,
  javascript({ typescript: true }),
  autocompletion({ override: [myCompletions] }),
];

Custom Theme

import { EditorView } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";

const myTheme = EditorView.theme({
  "&": {
    color: "#c9d1d9",
    backgroundColor: "#0d1117",
    fontSize: "14px",
    fontFamily: "'Fira Code', monospace",
    borderRadius: "8px",
    padding: "8px",
  },
  ".cm-content": {
    caretColor: "#58a6ff",
    padding: "8px 0",
  },
  ".cm-line": {
    padding: "0 8px",
  },
  ".cm-activeLine": {
    backgroundColor: "#161b22",
  },
  ".cm-gutters": {
    backgroundColor: "#0d1117",
    color: "#484f58",
    border: "none",
  },
});

const myHighlightStyle = HighlightStyle.define([
  { tag: t.keyword, color: "#ff7b72" },
  { tag: t.string, color: "#a5d6ff" },
  { tag: t.comment, color: "#8b949e", fontStyle: "italic" },
  { tag: t.function(t.variableName), color: "#d2a8ff" },
  { tag: t.number, color: "#79c0ff" },
]);

const extensions = [
  myTheme,
  syntaxHighlighting(myHighlightStyle),
];
// npm install @uiw/react-codemirror @codemirror/lang-javascript
import CodeMirror from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";

export function SimpleEditor() {
  return (
    <CodeMirror
      value={`const greeting = "Hello, World!";`}
      height="300px"
      extensions={[javascript({ jsx: true })]}
      theme={oneDark}
      onChange={(value) => console.log(value)}
    />
  );
}

Sandpack: Live Execution Playgrounds

Sandpack from CodeSandbox embeds a full browser-based runtime — it can actually run and hot-reload React/JavaScript code in the browser without any server.

Installation

npm install @codesandbox/sandpack-react

Basic Playground

import { Sandpack } from "@codesandbox/sandpack-react";

export function ReactPlayground() {
  return (
    <Sandpack
      template="react-ts"
      theme="dark"
      options={{
        showNavigator: false,
        showTabs: true,
        showLineNumbers: true,
        editorHeight: 400,
      }}
    />
  );
}

Custom Files

import { Sandpack } from "@codesandbox/sandpack-react";

export function CustomPlayground() {
  return (
    <Sandpack
      template="react-ts"
      files={{
        "/App.tsx": {
          code: `import { useState } from "react";
import { Counter } from "./Counter";

export default function App() {
  return (
    <div style={{ padding: 20 }}>
      <h1>Live Counter</h1>
      <Counter initial={0} />
    </div>
  );
}`,
        },
        "/Counter.tsx": {
          code: `import { useState } from "react";

interface CounterProps {
  initial: number;
}

export function Counter({ initial }: CounterProps) {
  const [count, setCount] = useState(initial);
  return (
    <div>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <span style={{ margin: "0 16px" }}>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}`,
          active: true, // This file opens first
        },
      }}
      theme="dark"
      options={{
        showTabs: true,
        showNavigator: false,
        editorHeight: 350,
      }}
    />
  );
}

Custom Sandpack Layout

import {
  SandpackProvider,
  SandpackLayout,
  SandpackCodeEditor,
  SandpackPreview,
  SandpackConsole,
  useSandpack,
} from "@codesandbox/sandpack-react";

// Custom reset button using the useSandpack hook
function ResetButton() {
  const { sandpack } = useSandpack();
  return (
    <button onClick={() => sandpack.resetAllFiles()}>
      Reset
    </button>
  );
}

export function CustomSandpackLayout() {
  return (
    <SandpackProvider
      template="react-ts"
      theme="dark"
      files={{
        "/App.tsx": `export default function App() { return <h1>Hello!</h1>; }`,
      }}
    >
      <div style={{ border: "1px solid #333", borderRadius: 8, overflow: "hidden" }}>
        <div style={{ background: "#1a1a1a", padding: "8px 16px", display: "flex", justifyContent: "flex-end" }}>
          <ResetButton />
        </div>
        <SandpackLayout>
          <SandpackCodeEditor
            showTabs
            showLineNumbers
            showInlineErrors
            wrapContent
          />
          <div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
            <SandpackPreview style={{ flex: 2 }} />
            <SandpackConsole style={{ flex: 1, maxHeight: 120 }} />
          </div>
        </SandpackLayout>
      </div>
    </SandpackProvider>
  );
}

Custom Dependencies

import { Sandpack } from "@codesandbox/sandpack-react";

export function ZustandPlayground() {
  return (
    <Sandpack
      template="react-ts"
      customSetup={{
        dependencies: {
          zustand: "^5.0.0",
          immer: "^10.0.0",
        },
      }}
      files={{
        "/App.tsx": `import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

const useStore = create(
  immer((set) => ({
    count: 0,
    increment: () => set((state) => { state.count++; }),
    decrement: () => set((state) => { state.count--; }),
  }))
);

export default function App() {
  const { count, increment, decrement } = useStore();
  return (
    <div style={{ padding: 20 }}>
      <h1>Zustand + Immer Counter: {count}</h1>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}`,
      }}
      theme="dark"
    />
  );
}

Feature Comparison

FeatureMonaco EditorCodeMirror 6Sandpack
Bundle size~5MB gzipped~50-200kB~3MB (runtime)
TypeScript IntelliSense✅ Full LSP❌ Syntax only❌ Syntax only
Code execution✅ Runs in browser
Hot reload preview
Custom languages✅ TextMate grammars✅ Lezer grammars✅ (via CodeMirror)
AccessibilityBasic✅ Excellent✅ (via CodeMirror)
Mobile support⚠️ Limited
SSR-safe❌ Needs dynamic import⚠️ Needs dynamic import
React integration@monaco-editor/react@uiw/react-codemirrorNative
Diff view✅ Built-inPlugin availableNo
Multi-fileVia state✅ Built-in tabs
Custom themes
GitHub stars38k7.5k4k
npm weekly2M5M500k
Maintained byMicrosoftMarijn HaverbekeCodeSandbox

When to Use Each

Choose Monaco Editor if:

  • Building a browser-based IDE or TypeScript playground
  • Need full IntelliSense: hover types, go-to-definition, rename symbol
  • Target users are primarily desktop (bundle size acceptable)
  • Building something like StackBlitz, TypeScript Playground, or Azure Portal
  • Diff editor for code review is needed

Choose CodeMirror 6 if:

  • Custom editor in a product (admin SQL editor, config editor, notebook cells)
  • Bundle size is important (need to ship fast on mobile)
  • Need excellent accessibility (keyboard users, screen readers)
  • Building a custom language integration with Lezer grammar
  • Embedding a lightweight editor in documentation
  • Need fine-grained control over every extension and behavior

Choose Sandpack if:

  • Interactive documentation with live code examples
  • Tutorial or course platform where learners run code
  • React/JavaScript playgrounds (like React's own documentation)
  • Prototyping tool where non-technical users modify code
  • Blog posts with runnable code examples
  • No need to run arbitrary server-side code — browser only

Methodology

Data sourced from Monaco Editor documentation (microsoft.github.io/monaco-editor), CodeMirror 6 documentation (codemirror.net), Sandpack documentation (sandpack.codesandbox.io), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and bundle size measurements from bundlephobia.com and direct testing.


Related: tiptap vs Lexical vs Slate vs Quill rich text editors for rich text editing (not code), or StackBlitz vs CodeSandbox vs Gitpod cloud development for full online IDE platforms.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.