Monaco Editor brings VS Code’s editing experience to the browser — <Editor language="typescript" value={code} onChange={setValue} theme="vs-dark"> renders the editor. onMount={(editor, monaco) => { editorRef.current = editor }} captures the instance. editor.addAction({ id, label, keybindings, run }) adds custom keyboard commands. monaco.languages.registerCompletionItemProvider adds custom autocomplete. monaco.languages.typescript.typescriptDefaults.addExtraLib(dts, "types/lib.d.ts") injects TypeScript definitions. <DiffEditor original={oldCode} modified={newCode}> shows a two-pane diff view. editor.layout({ width, height }) forces resize. monaco.editor.createModel(code, language, uri) creates multi-model tabs. editor.setValue/getValue/getPosition are the core imperative API. defaultOptions: { minimap: { enabled: false }, fontSize: 14, lineNumbers: "on" } configures the editor. Claude Code generates Monaco-based code editors, playgrounds, diff viewers, and SQL consoles.
CLAUDE.md for Monaco Editor
## Monaco Editor Stack
- Version: @monaco-editor/react >= 4.6, monaco-editor >= 0.50
- Basic: <Editor height="400px" language="typescript" value={code} onChange={(v) => setCode(v ?? "")} theme="vs-dark" options={{ minimap: { enabled: false }, fontSize: 14, tabSize: 2 }} />
- Mount: onMount={(editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco }}
- Resize: useEffect(() => { editorRef.current?.layout() }, [panelWidth])
- Action: editor.addAction({ id: "run-code", label: "Run Code", keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], run: handleRun })
- TypeScript dts: monaco.languages.typescript.typescriptDefaults.addExtraLib(content, "ts:filename/types.d.ts")
- Diff: <DiffEditor original={before} modified={after} language="json" theme="vs-dark" />
Code Playground Component
// components/editor/CodePlayground.tsx — Monaco editor with run + output pane
"use client"
import { useRef, useState, useCallback } from "react"
import Editor, { DiffEditor } from "@monaco-editor/react"
import type { editor as monacoEditor, IRange } from "monaco-editor"
import type * as Monaco from "monaco-editor"
type Language = "typescript" | "javascript" | "python" | "json" | "css" | "html" | "sql" | "yaml" | "markdown"
const LANGUAGE_DEFAULTS: Partial<Record<Language, string>> = {
typescript: `// TypeScript playground
function greet(name: string): string {
return \`Hello, \${name}!\`
}
console.log(greet("World"))`,
json: `{
"name": "example",
"version": "1.0.0",
"scripts": {
"dev": "next dev"
}
}`,
sql: `SELECT
users.name,
COUNT(orders.id) AS order_count,
SUM(orders.total) AS revenue
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
ORDER BY revenue DESC
LIMIT 10;`,
}
interface CodePlaygroundProps {
initialLanguage?: Language
initialCode?: string
readOnly?: boolean
showDiff?: boolean
onRun?: (code: string) => Promise<string>
}
export function CodePlayground({
initialLanguage = "typescript",
initialCode,
readOnly = false,
showDiff = false,
onRun,
}: CodePlaygroundProps) {
const editorRef = useRef<monacoEditor.IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<typeof Monaco | null>(null)
const [code, setCode] = useState(initialCode ?? LANGUAGE_DEFAULTS[initialLanguage] ?? "")
const [language, setLanguage] = useState<Language>(initialLanguage)
const [output, setOutput] = useState<string | null>(null)
const [isRunning, setIsRunning] = useState(false)
const [theme, setTheme] = useState<"vs-dark" | "light">("vs-dark")
const handleMount = useCallback((editor: monacoEditor.IStandaloneCodeEditor, monaco: typeof Monaco) => {
editorRef.current = editor
monacoRef.current = monaco
// Custom run action (Ctrl/Cmd+Enter)
editor.addAction({
id: "run-code",
label: "Run Code",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run: () => handleRun(),
})
// Format on save (Ctrl/Cmd+S)
editor.addAction({
id: "format-code",
label: "Format Code",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
run: (ed) => {
ed.getAction("editor.action.formatDocument")?.run()
},
})
// Configure TypeScript
if (language === "typescript" || language === "javascript") {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
strict: true,
target: monaco.languages.typescript.ScriptTarget.ES2022,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
})
}
}, [language])
const handleRun = useCallback(async () => {
if (!onRun || isRunning) return
setIsRunning(true)
setOutput(null)
try {
const result = await onRun(editorRef.current?.getValue() ?? code)
setOutput(result)
} catch (e) {
setOutput(`Error: ${(e as Error).message}`)
} finally {
setIsRunning(false)
}
}, [onRun, code, isRunning])
const handleLanguageChange = useCallback((lang: Language) => {
setLanguage(lang)
if (LANGUAGE_DEFAULTS[lang]) setCode(LANGUAGE_DEFAULTS[lang]!)
}, [])
const addDecoration = useCallback((range: IRange, message: string) => {
editorRef.current?.deltaDecorations([], [{
range,
options: {
isWholeLine: true,
className: "editor-error-line",
glyphMarginClassName: "editor-error-glyph",
glyphMarginHoverMessage: { value: message },
},
}])
}, [])
const LANGUAGES: Language[] = ["typescript", "javascript", "python", "json", "sql", "yaml", "html", "css", "markdown"]
return (
<div className="flex flex-col rounded-2xl border overflow-hidden bg-[#1e1e1e]">
{/* Toolbar */}
<div className="flex items-center gap-2 px-4 py-2 border-b border-white/10">
{/* Language selector */}
<select
value={language}
onChange={(e) => handleLanguageChange(e.target.value as Language)}
className="text-xs bg-white/10 text-white border border-white/20 rounded-md px-2 py-1 focus:outline-none"
>
{LANGUAGES.map((l) => (
<option key={l} value={l}>{l}</option>
))}
</select>
{/* Theme toggle */}
<button
onClick={() => setTheme((t) => t === "vs-dark" ? "light" : "vs-dark")}
className="text-xs text-white/60 hover:text-white px-2 py-1 rounded"
>
{theme === "vs-dark" ? "☀️ Light" : "🌙 Dark"}
</button>
<div className="flex-1" />
{onRun && (
<button
onClick={handleRun}
disabled={isRunning}
className="flex items-center gap-1.5 text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors disabled:opacity-50"
>
{isRunning ? "⏳ Running…" : "▶ Run"}
<span className="text-white/50 text-[10px]">⌘↩</span>
</button>
)}
</div>
{/* Editor */}
{showDiff ? (
<DiffEditor
height="400px"
language={language}
original={initialCode ?? ""}
modified={code}
theme={theme}
options={{ readOnly, minimap: { enabled: false }, fontSize: 13 }}
/>
) : (
<Editor
height="400px"
language={language}
value={code}
theme={theme}
onChange={(v) => setCode(v ?? "")}
onMount={handleMount}
options={{
readOnly,
minimap: { enabled: false },
fontSize: 13,
lineNumbers: "on",
renderLineHighlight: "all",
scrollBeyondLastLine: false,
wordWrap: "on",
tabSize: 2,
suggestOnTriggerCharacters: true,
quickSuggestions: { other: true, comments: false, strings: false },
padding: { top: 12, bottom: 12 },
}}
/>
)}
{/* Output pane */}
{output !== null && (
<div className="border-t border-white/10">
<div className="flex items-center justify-between px-4 py-2 bg-black/20">
<span className="text-xs text-white/60 font-mono">Output</span>
<button onClick={() => setOutput(null)} className="text-xs text-white/40 hover:text-white">✕</button>
</div>
<pre className="px-4 py-3 text-xs font-mono text-green-400 max-h-48 overflow-auto whitespace-pre-wrap">
{output}
</pre>
</div>
)}
</div>
)
}
SQL Editor with Custom Autocomplete
// components/editor/SqlEditor.tsx — Monaco SQL editor with schema autocomplete
"use client"
import { useRef, useEffect } from "react"
import Editor from "@monaco-editor/react"
import type * as Monaco from "monaco-editor"
type TableSchema = {
name: string
columns: { name: string; type: string }[]
}
interface SqlEditorProps {
value: string
onChange: (sql: string) => void
schema?: TableSchema[]
height?: string
}
export function SqlEditor({ value, onChange, schema = [], height = "200px" }: SqlEditorProps) {
const monacoRef = useRef<typeof Monaco | null>(null)
const disposeRef = useRef<Monaco.IDisposable | null>(null)
const handleMount = (_: any, monaco: typeof Monaco) => {
monacoRef.current = monaco
registerSqlCompletion(monaco, schema)
}
useEffect(() => {
if (monacoRef.current) {
disposeRef.current?.dispose()
disposeRef.current = registerSqlCompletion(monacoRef.current, schema)
}
return () => disposeRef.current?.dispose()
}, [schema])
return (
<Editor
height={height}
language="sql"
value={value}
onChange={(v) => onChange(v ?? "")}
theme="vs-dark"
onMount={handleMount}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbers: "on",
wordWrap: "on",
scrollBeyondLastLine: false,
padding: { top: 8, bottom: 8 },
}}
/>
)
}
function registerSqlCompletion(monaco: typeof Monaco, schema: TableSchema[]) {
return monaco.languages.registerCompletionItemProvider("sql", {
triggerCharacters: [" ", ".", "\n"],
provideCompletionItems(model, position) {
const word = model.getWordUntilPosition(position)
const range: Monaco.IRange = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
const suggestions: Monaco.languages.CompletionItem[] = []
// Table name suggestions
schema.forEach((table) => {
suggestions.push({
label: table.name,
kind: monaco.languages.CompletionItemKind.Class,
insertText: table.name,
detail: `Table (${table.columns.length} columns)`,
range,
})
// Column suggestions
table.columns.forEach((col) => {
suggestions.push({
label: `${table.name}.${col.name}`,
kind: monaco.languages.CompletionItemKind.Field,
insertText: col.name,
detail: `${col.type} — ${table.name}`,
documentation: `Column of type ${col.type} from table ${table.name}`,
range,
})
})
})
return { suggestions }
},
})
}
For the CodeMirror 6 alternative when a lighter bundle, mobile-first touch support, a more modular extension system (every feature is a separate package), or native React integration without a wrapper library is preferred — CodeMirror 6 has a smaller footprint while Monaco provides a complete IDE experience closest to VS Code, see the CodeMirror guide. For the Ace Editor alternative when legacy browser support, a smaller bundle without web workers, or an older established codebase using Ace is the constraint — Ace is battle-tested but less actively developed compared to Monaco’s continuous VS Code alignment, see the Ace guide. The Claude Skills 360 bundle includes Monaco Editor skill sets covering language configuration, custom autocomplete, and diff views. Start with the free tier to try browser IDE generation.