Claude Code for Monaco Editor: VS Code in the Browser — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Monaco Editor: VS Code in the Browser
Frontend

Claude Code for Monaco Editor: VS Code in the Browser

Published: June 21, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free