CodeMirror 6 is a modular, mobile-ready code editor — EditorState.create({ doc, extensions: [javascript(), oneDark, keymap.of(defaultKeymap)] }) creates editor state. new EditorView({ state, parent: containerEl }) mounts the editor. EditorView.updateListener.of((update) => { if (update.docChanged) onChange(update.state.doc.toString()) }) fires on changes. Language packs: javascript({ typescript: true }), python(), sql(), markdown(), json(). Themes: oneDark, @codemirror/theme-github-light (third-party). keymap.of([{ key: "Mod-Enter", run: handleRun }]) adds keyboard shortcuts. StateEffect.define() + StateField.define() creates custom state. Decoration.mark({ attributes: { class } }) highlights ranges. autocompletion({ override: [myCompletions] }) adds custom autocomplete. linter(async (view) => diagnostics) adds live linting. compartment.reconfigure(language) hot-swaps extensions. Claude Code generates CodeMirror editors, SQL consoles, note editors, and inline code fields.
CLAUDE.md for CodeMirror 6
## CodeMirror 6 Stack
- Version: @codemirror/view >= 6.35, @codemirror/state >= 6.5
- Bundles: @codemirror/basic-setup OR individual packages
- React: const editorRef = useRef<HTMLDivElement>(null); const viewRef = useRef<EditorView>(); useEffect(() => { const view = new EditorView({ state: EditorState.create({ doc, extensions }), parent: editorRef.current! }); viewRef.current = view; return () => view.destroy() }, [])
- Change value: viewRef.current?.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: newValue } })
- Language: import { javascript } from "@codemirror/lang-javascript"; use in extensions array
- Compartment: const langCompartment = new Compartment(); extensions: [langCompartment.of(javascript())]; swap: view.dispatch({ effects: langCompartment.reconfigure(python()) })
useCodeMirror Hook
// lib/hooks/useCodeMirror.ts — CodeMirror 6 React integration
import { useEffect, useRef, type RefObject } from "react"
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from "@codemirror/view"
import { EditorState, Compartment } from "@codemirror/state"
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands"
import { indentOnInput, bracketMatching, foldGutter, foldKeymap } from "@codemirror/language"
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from "@codemirror/autocomplete"
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"
import { oneDark } from "@codemirror/theme-one-dark"
import type { Extension, Transaction } from "@codemirror/state"
import type { LanguageSupport } from "@codemirror/language"
type UseCodeMirrorOptions = {
value: string
onChange?: (value: string) => void
language?: LanguageSupport
theme?: "dark" | "light"
readOnly?: boolean
placeholder?: string
extraExtensions?: Extension[]
}
export function useCodeMirror(options: UseCodeMirrorOptions): RefObject<HTMLDivElement | null> {
const {
value,
onChange,
language,
theme = "dark",
readOnly = false,
extraExtensions = [],
} = options
const containerRef = useRef<HTMLDivElement | null>(null)
const viewRef = useRef<EditorView | null>(null)
const onChangeRef = useRef(onChange)
onChangeRef.current = onChange
// Track external value changes without recreating the editor
const externalValueRef = useRef(value)
useEffect(() => {
if (!containerRef.current) return
const updateListener = EditorView.updateListener.of((update: { docChanged: boolean; state: { doc: { toString: () => string } } }) => {
if (update.docChanged) {
const newValue = update.state.doc.toString()
externalValueRef.current = newValue
onChangeRef.current?.(newValue)
}
})
const baseExtensions: Extension[] = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
indentWithTab,
]),
EditorState.readOnly.of(readOnly),
updateListener,
...(theme === "dark" ? [oneDark] : []),
...(language ? [language] : []),
...extraExtensions,
]
const state = EditorState.create({ doc: value, extensions: baseExtensions })
const view = new EditorView({ state, parent: containerRef.current })
viewRef.current = view
return () => {
view.destroy()
viewRef.current = null
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme, readOnly, language]) // Recreate only when theme/readOnly/language changes
// Sync external value changes without recreating the editor
useEffect(() => {
const view = viewRef.current
if (!view || value === externalValueRef.current) return
externalValueRef.current = value
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
})
}, [value])
return containerRef
}
Code Editor Component
// components/editor/CodeEditor.tsx — multi-language CodeMirror editor
"use client"
import { useState, useMemo } from "react"
import { javascript } from "@codemirror/lang-javascript"
import { python } from "@codemirror/lang-python"
import { sql } from "@codemirror/lang-sql"
import { json } from "@codemirror/lang-json"
import { markdown } from "@codemirror/lang-markdown"
import { css } from "@codemirror/lang-css"
import { html } from "@codemirror/lang-html"
import type { LanguageSupport } from "@codemirror/language"
import { useCodeMirror } from "@/lib/hooks/useCodeMirror"
type Lang = "typescript" | "javascript" | "python" | "sql" | "json" | "markdown" | "css" | "html"
const LANG_MAP: Record<Lang, () => LanguageSupport> = {
typescript: () => javascript({ typescript: true, jsx: true }),
javascript: () => javascript({ jsx: true }),
python,
sql,
json,
markdown,
css,
html,
}
const LANG_PLACEHOLDERS: Partial<Record<Lang, string>> = {
sql: "SELECT * FROM users WHERE active = true;",
json: '{ "key": "value" }',
python: "def hello():\n print('Hello, World!')",
}
interface CodeEditorProps {
value: string
onChange: (value: string) => void
language?: Lang
readOnly?: boolean
theme?: "dark" | "light"
height?: string
showLanguagePicker?: boolean
}
export function CodeEditor({
value,
onChange,
language: initialLang = "typescript",
readOnly = false,
theme = "dark",
height = "300px",
showLanguagePicker = true,
}: CodeEditorProps) {
const [language, setLanguage] = useState<Lang>(initialLang)
const languageExt = useMemo(() => LANG_MAP[language](), [language])
const containerRef = useCodeMirror({
value,
onChange,
language: languageExt,
theme,
readOnly,
})
const bgColor = theme === "dark" ? "#282c34" : "#fafafa"
return (
<div className="rounded-xl overflow-hidden border">
{showLanguagePicker && (
<div
className="flex items-center gap-2 px-3 py-2 border-b text-xs"
style={{ backgroundColor: bgColor, borderColor: theme === "dark" ? "#3d3d3d" : "#e5e7eb" }}
>
<span style={{ color: theme === "dark" ? "#abb2bf" : "#6b7280" }}>Language:</span>
<select
value={language}
onChange={(e) => setLanguage(e.target.value as Lang)}
className="text-xs border rounded px-1.5 py-0.5 focus:outline-none"
style={{
backgroundColor: theme === "dark" ? "#3d3d3d" : "#f3f4f6",
color: theme === "dark" ? "#abb2bf" : "#374151",
borderColor: theme === "dark" ? "#555" : "#d1d5db",
}}
>
{(Object.keys(LANG_MAP) as Lang[]).map((l) => (
<option key={l} value={l}>{l}</option>
))}
</select>
{readOnly && (
<span style={{ color: theme === "dark" ? "#6b7280" : "#9ca3af" }} className="ml-auto">
Read only
</span>
)}
</div>
)}
<div
ref={containerRef}
style={{
height,
overflowY: "auto",
fontSize: "13px",
}}
/>
</div>
)
}
Linting Extension
// lib/editor/linting.ts — CodeMirror linter with diagnostics
import { linter, type Diagnostic } from "@codemirror/lint"
import type { EditorView } from "@codemirror/view"
// JSON linter that highlights parse errors
export const jsonLinter = linter((view: EditorView): Diagnostic[] => {
const text = view.state.doc.toString()
if (!text.trim()) return []
try {
JSON.parse(text)
return []
} catch (e: unknown) {
const message = e instanceof SyntaxError ? e.message : "JSON parse error"
// Parse position from error message like "Unexpected token } in JSON at position 42"
const posMatch = message.match(/position (\d+)/)
const pos = posMatch ? parseInt(posMatch[1]) : 0
return [{
from: Math.min(pos, text.length - 1),
to: Math.min(pos + 1, text.length),
severity: "error",
message,
}]
}
})
For the Monaco Editor alternative when full VS Code feature parity, TypeScript IntelliSense with type-checking in the browser, a richer built-in diff editor, or language server protocol support is required — Monaco is heavier (~2MB) but provides the most complete IDE editing experience, while CodeMirror 6 is lighter (tree-shakeable to ~100KB), better on mobile, and more extensible at the core architecture level, see the Monaco Editor guide. For the Ace Editor alternative when supporting legacy browsers without ES modules is required or an older codebase already depends on Ace — Ace offers broad browser compatibility while CodeMirror 6 requires modern environments with good module support, see the Ace guide. The Claude Skills 360 bundle includes CodeMirror 6 skill sets covering language packs, custom extensions, and linting. Start with the free tier to try lightweight editor generation.