Monaco Editor vs CodeMirror 6 vs Sandpack: In-Browser Code Editors 2026
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
MonacoWebpackPluginor 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),
];
Or use @uiw/react-codemirror (Popular React Wrapper)
// 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
| Feature | Monaco Editor | CodeMirror 6 | Sandpack |
|---|---|---|---|
| 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) |
| Accessibility | Basic | ✅ Excellent | ✅ (via CodeMirror) |
| Mobile support | ⚠️ Limited | ✅ | ✅ |
| SSR-safe | ❌ Needs dynamic import | ✅ | ⚠️ Needs dynamic import |
| React integration | @monaco-editor/react | @uiw/react-codemirror | Native |
| Diff view | ✅ Built-in | Plugin available | No |
| Multi-file | ✅ | Via state | ✅ Built-in tabs |
| Custom themes | ✅ | ✅ | ✅ |
| GitHub stars | 38k | 7.5k | 4k |
| npm weekly | 2M | 5M | 500k |
| Maintained by | Microsoft | Marijn Haverbeke | CodeSandbox |
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.