Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
Part of: React & Frontend Errors
Quick Answer
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
The Problem
CodeMirror mounts but shows nothing:
import { EditorView, basicSetup } from 'codemirror';
const view = new EditorView({
parent: document.getElementById('editor')!,
});
// Empty editor — no gutters, no highlightingOr the React wrapper doesn’t sync with state:
const [code, setCode] = useState('hello');
// Editor shows 'hello' but typing doesn't update `code`Or language highlighting doesn’t apply:
JavaScript code renders as plain text — no colorsWhy This Happens
CodeMirror 6 is a modular editor — it ships with almost nothing by default. Every feature is an extension, and the boundary between the editor core and language/theme packages is sharper than in any other browser editor. That design pays off for bundle size, but it also means that copy-pasting a CodeMirror 5 example into a v6 project produces a blank <div> and no error.
The second source of confusion is React. CodeMirror manages its own document state in a Transaction system that is not React’s, so trying to treat the editor as a controlled input (value={code} driven by setCode) creates an infinite loop or a frozen cursor. The supported pattern is uncontrolled with explicit sync, via EditorView.updateListener and Compartment.reconfigure.
Common causes:
basicSetupprovides essential features — without it, you get a plain textarea with no line numbers, no syntax highlighting, and no keyboard shortcuts.basicSetupbundles line numbers, undo/redo, bracket matching, and other fundamentals.- Languages are separate packages —
@codemirror/lang-javascript,@codemirror/lang-python, etc. must be installed and added to the extensions array. Without a language extension, code is plain text. - CodeMirror manages its own state — unlike a controlled React input, CodeMirror maintains internal state. You must use
EditorView.updateListeneror a dispatch handler to sync with React state. Directly settingvaluefrom React does not work without reconciliation. - Themes are extensions, not CSS — CodeMirror 6 uses its own styling system. A theme is an extension that defines editor colors. Without a theme extension, the editor uses minimal browser defaults.
- CM5 vs CM6 packages coexist on npm —
[email protected](legacy, single package) and[email protected](the meta-package over@codemirror/*modules) ship side by side. Installingcodemirrorwithout a version pin can land you on whichever the registry currently labelslatest.
Version History That Changes the Failure Mode
CodeMirror 6 was a complete rewrite, and the editor you are using depends entirely on which line you installed.
- CodeMirror 5 (2013–present, maintenance mode) — single-package CommonJS bundle. Imported as
import CodeMirror from 'codemirror'and configured by passing a giant options object. Language modes live undercodemirror/mode/*and are loaded at runtime by adding<script>tags orrequire(). Theming is pure CSS. Stable, large bundle, still used by older Jupyter notebooks and Bitbucket. - CodeMirror 6 (June 2021) — ESM-only rewrite. The editor is split into roughly twenty packages under
@codemirror/*(view, state, language, commands, autocomplete, lint, search, theme-one-dark, lang-javascript, lang-python, etc.). State is immutable and updated via transactions. Extensions are the only configuration surface. CM5 examples will not run. - CodeMirror 6.5 (mid 2023) — added hover tooltips (
hoverTooltip) and improved input method (IME) handling on Chinese/Japanese keyboards. If you are stuck on a 6.0–6.4 release, tooltip APIs you find in tutorials may not exist. - CodeMirror 6.6 (late 2023) — better RTL support, performance fixes for very long lines, and improved gutter customization. Custom gutter code from 6.0 may need tweaks.
- CodeMirror 6.0.x of
@codemirror/lang-*— the language packages have their own semver.@codemirror/lang-javascriptv6.2+ added TypeScript-in-JS support via thetypescript: trueoption. Earlier versions ignored the flag. Mismatches betweencodemirror(the meta-package) and individual@codemirror/lang-*versions are silent — the editor just falls back to plain text. @uiw/react-codemirror(community React wrapper) — tracks CM6 closely. v4.x targets CM6.0–6.2, v4.20+ targets CM6.3+. If yourthemeprop is ignored, you may be on an old wrapper version that pre-dates the theme prop.- Ecosystem comparison — Monaco Editor (VS Code’s editor) is heavier and uses Web Workers for language services. Ace Editor is the old standard, still actively maintained but architecturally similar to CM5. CodeMirror 6 is the smallest of the three, at the cost of demanding more wiring.
If npm install codemirror lands you on v5 and your tutorial assumes v6, the editor mounts with the wrong API surface entirely. Pin "codemirror": "^6.0.0" explicitly.
Fix 1: Vanilla JavaScript Setup
npm install codemirror @codemirror/lang-javascript @codemirror/lang-python @codemirror/lang-html @codemirror/lang-css @codemirror/lang-json
npm install @codemirror/theme-one-darkimport { EditorView, basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
const state = EditorState.create({
doc: 'const greeting = "Hello, World!";\nconsole.log(greeting);',
extensions: [
basicSetup, // Line numbers, undo, brackets, etc.
javascript({ typescript: true, jsx: true }), // Language support
oneDark, // Theme
EditorView.lineWrapping, // Word wrap
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newCode = update.state.doc.toString();
console.log('Code changed:', newCode);
}
}),
],
});
// Mount — parent element MUST exist and have dimensions
const view = new EditorView({
state,
parent: document.getElementById('editor')!,
});
// Update content programmatically
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: 'new content' },
});
// Cleanup
view.destroy();Fix 2: React Integration
npm install @uiw/react-codemirror
# Or build your own wrapper (see Fix 3)// Using @uiw/react-codemirror (simplest)
'use client';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { useState } from 'react';
function Editor() {
const [code, setCode] = useState('const x: number = 42;');
return (
<CodeMirror
value={code}
height="400px"
theme={oneDark}
extensions={[javascript({ typescript: true, jsx: true })]}
onChange={(value) => setCode(value)}
basicSetup={{
lineNumbers: true,
highlightActiveLineGutter: true,
foldGutter: true,
autocompletion: true,
bracketMatching: true,
closeBrackets: true,
indentOnInput: true,
}}
/>
);
}Fix 3: Custom React Wrapper
// components/CodeEditor.tsx — full control
'use client';
import { useRef, useEffect, useState } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState, type Extension } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
const languageMap: Record<string, () => Extension> = {
javascript: () => javascript({ typescript: false, jsx: true }),
typescript: () => javascript({ typescript: true, jsx: true }),
python: () => python(),
html: () => html(),
css: () => css(),
json: () => json(),
};
interface CodeEditorProps {
value: string;
onChange?: (value: string) => void;
language?: string;
readOnly?: boolean;
height?: string;
}
export function CodeEditor({
value,
onChange,
language = 'typescript',
readOnly = false,
height = '400px',
}: CodeEditorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
useEffect(() => {
if (!containerRef.current) return;
const langExtension = languageMap[language]?.() ?? javascript({ typescript: true });
const state = EditorState.create({
doc: value,
extensions: [
basicSetup,
langExtension,
oneDark,
EditorView.lineWrapping,
EditorState.readOnly.of(readOnly),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChangeRef.current?.(update.state.doc.toString());
}
}),
],
});
const view = new EditorView({ state, parent: containerRef.current });
viewRef.current = view;
return () => {
view.destroy();
viewRef.current = null;
};
}, [language, readOnly]); // Recreate on language or readOnly change
// Sync external value changes
useEffect(() => {
const view = viewRef.current;
if (!view) return;
const currentValue = view.state.doc.toString();
if (currentValue !== value) {
view.dispatch({
changes: { from: 0, to: currentValue.length, insert: value },
});
}
}, [value]);
return <div ref={containerRef} style={{ height, overflow: 'auto' }} />;
}Fix 4: Custom Extensions
import { keymap } from '@codemirror/view';
import { indentWithTab } from '@codemirror/commands';
import { EditorView } from 'codemirror';
// Tab indentation (not enabled by default)
const tabExtension = keymap.of([indentWithTab]);
// Custom keybindings
const customKeymap = keymap.of([
{
key: 'Mod-s',
run: (view) => {
const code = view.state.doc.toString();
handleSave(code);
return true;
},
},
{
key: 'Mod-Enter',
run: (view) => {
const code = view.state.doc.toString();
handleRun(code);
return true;
},
},
]);
// Read-only mode
import { EditorState } from '@codemirror/state';
const readOnly = EditorState.readOnly.of(true);
// Custom styling
const customTheme = EditorView.theme({
'&': {
fontSize: '14px',
border: '1px solid #333',
borderRadius: '8px',
},
'.cm-content': {
fontFamily: '"Fira Code", monospace',
padding: '12px',
},
'.cm-gutters': {
backgroundColor: '#1a1a2e',
borderRight: '1px solid #333',
},
'.cm-activeLineGutter': {
backgroundColor: '#2a2a4e',
},
'&.cm-focused .cm-cursor': {
borderLeftColor: '#60a5fa',
},
'.cm-selectionBackground': {
backgroundColor: '#3a3a6e !important',
},
});
// Combine extensions
const extensions = [
basicSetup,
javascript({ typescript: true }),
oneDark,
tabExtension,
customKeymap,
customTheme,
EditorView.lineWrapping,
];Fix 5: Autocomplete and Linting
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
import { linter, type Diagnostic } from '@codemirror/lint';
// Custom autocomplete
function myCompletions(context: CompletionContext) {
const word = context.matchBefore(/\w*/);
if (!word || (word.from === word.to && !context.explicit)) return null;
return {
from: word.from,
options: [
{ label: 'console.log', type: 'function', detail: 'Log to console' },
{ label: 'useState', type: 'function', detail: 'React hook' },
{ label: 'useEffect', type: 'function', detail: 'React hook' },
{ label: 'async', type: 'keyword' },
{ label: 'await', type: 'keyword' },
],
};
}
const completionExtension = autocompletion({ override: [myCompletions] });
// Custom linter
const myLinter = linter((view) => {
const diagnostics: Diagnostic[] = [];
const text = view.state.doc.toString();
// Check for console.log
const regex = /console\.log/g;
let match;
while ((match = regex.exec(text)) !== null) {
diagnostics.push({
from: match.index,
to: match.index + match[0].length,
severity: 'warning',
message: 'Remove console.log before committing',
});
}
return diagnostics;
});
// Add to extensions
const extensions = [basicSetup, completionExtension, myLinter];Fix 6: Dynamic Language Switching
'use client';
import { useState, useRef, useEffect } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState, Compartment } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
function DynamicLanguageEditor() {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const langCompartment = useRef(new Compartment());
const [language, setLanguage] = useState('typescript');
useEffect(() => {
if (!containerRef.current) return;
const view = new EditorView({
state: EditorState.create({
doc: '// Start typing...',
extensions: [
basicSetup,
langCompartment.current.of(javascript({ typescript: true })),
],
}),
parent: containerRef.current,
});
viewRef.current = view;
return () => view.destroy();
}, []);
// Switch language without recreating the editor
useEffect(() => {
const view = viewRef.current;
if (!view) return;
const langMap: Record<string, any> = {
typescript: javascript({ typescript: true, jsx: true }),
javascript: javascript({ jsx: true }),
python: python(),
html: html(),
};
view.dispatch({
effects: langCompartment.current.reconfigure(langMap[language] || javascript()),
});
}, [language]);
return (
<div>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="typescript">TypeScript</option>
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="html">HTML</option>
</select>
<div ref={containerRef} style={{ height: '400px' }} />
</div>
);
}Still Not Working?
Editor renders but no syntax highlighting — you need a language extension. basicSetup provides editor features but not language support. Install and add @codemirror/lang-javascript (or the appropriate language) to extensions.
React state does not update when typing — CodeMirror manages its own state. Use EditorView.updateListener.of() to listen for changes and update React state. Do not try to make CodeMirror a controlled component — treat it as uncontrolled with sync.
Editor flickers or recreates on every render — the EditorView should be created once in useEffect, not on every render. Store it in a ref. For dynamic changes (language, theme), use Compartment to reconfigure without recreating.
Tab key inserts focus change instead of indent — basicSetup does not include tab-to-indent to preserve accessibility. Import and add keymap.of([indentWithTab]) from @codemirror/commands to enable tab indentation.
Editor mounts but has zero height — EditorView measures the parent at mount. If the container is display: none or height: 0 at the moment of construction, content lays out at zero height. Render the editor only after the container is visible, or call view.requestMeasure() once the layout settles.
Wrong CodeMirror version installed (CM5 vs CM6) — if import CodeMirror from 'codemirror' works and you can configure via an options object, you are on CM5. If you have to import EditorView and EditorState separately, you are on CM6. Pin the version in package.json and align your tutorial.
IME composition characters disappear (Japanese, Chinese, Korean input) — pre-6.5 versions had bugs with composition events. Upgrade to @codemirror/[email protected]+ and ensure no extension is calling view.dispatch synchronously during a compositionstart/compositionend window.
For related editor issues, see Fix: Monaco Editor Not Working, Fix: Tiptap Not Working, Fix: Prism React Renderer Not Working, and Fix: Shiki Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: ky Not Working — Requests Failing, Hooks Not Firing, or Retry Not Working
How to fix ky HTTP client issues — instance creation, hooks (beforeRequest, afterResponse), retry configuration, timeout handling, JSON parsing, error handling, and migration from fetch or axios.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.