CodeEditor
CodeEditor
A single component, two engines.
| Engine | Bundle | Strengths | Mobile |
|---|---|---|---|
codemirror (default) | ~150 KB gz | Touch-friendly, fast cold start, themeable via tokens | Excellent |
monaco | ~2 MB | Full IDE feel: hover, find/replace, multi-cursor, diff | Workable |
Both engines are declared as optional peer dependencies of @helixui/core. Install only what you use:
# CodeMirrorpnpm add codemirror @codemirror/state @codemirror/view @codemirror/commands \ @codemirror/language @codemirror/lang-javascript @codemirror/lang-json \ @codemirror/theme-one-dark
# Monacopnpm add monaco-editor @monaco-editor/reactThe base @helixui/core bundle does not pull in either until a <CodeEditor> actually mounts.
Basic
import { useState } from 'react';import { CodeEditor } from '@helixui/core';
const [code, setCode] = useState("const x = 1;\n");return <CodeEditor language="typescript" value={code} onChange={setCode} />;Switching engines
const [engine, setEngine] = useState<'codemirror' | 'monaco'>('codemirror');
<SegmentedControl selectedKeys={[engine]} onSelectionChange={(k) => setEngine([...k][0] as never)}> <Segment id="codemirror">CodeMirror</Segment> <Segment id="monaco">Monaco</Segment></SegmentedControl>
<CodeEditor engine={engine} language="typescript" value={code} onChange={setCode} />Imperative control via onMount
<CodeEditor engine="codemirror" value={code} onChange={setCode} onMount={({ engine, instance }) => { if (engine === 'codemirror') { const view = instance as import('@codemirror/view').EditorView; view.dispatch({ selection: { anchor: code.length } }); } }}/>CodeMirror — extension escape hatch
import { vim } from '@replit/codemirror-vim';import { lintGutter } from '@codemirror/lint';
<CodeEditor engine="codemirror" codemirrorExtensions={[vim(), lintGutter()]} value={code} onChange={setCode}/>Monaco — options escape hatch
<CodeEditor engine="monaco" monacoOptions={{ glyphMargin: true, folding: true, suggestOnTriggerCharacters: true }} value={code} onChange={setCode}/>Theming
theme="auto" watches document.documentElement.dataset.theme and retunes live. Toggle helixui’s theme provider and the editor swaps between oneDark (CodeMirror) or vs ↔ vs-dark (Monaco) without remounting.
Mobile
Below the helixui mobile breakpoint (≤767px):
- Default font drops to 13px.
- Word-wrap turns on by default.
- Line numbers turn off by default.
- Monaco’s minimap and right-click menu are disabled.
- Touch-scroll inertia is enabled on the CodeMirror scroller.
Override any of these defaults explicitly via the props.
Install: @helixui/core
import { CodeEditor } from '@helixui/core'status: stable · since: 0.6.0
Tags: code, editor, editable, codemirror, monaco
Anatomy
┌─ tsx ──────────────────────────────────┐│ 1 const x = 1; ← line nums ││ 2 console.log(x); ││ 3 | │└─────────────────────────────────────────┘Layout
- display —
block - width —
fill - height —
fixed:320px - intrinsicSize —
fills width, default 320px tall, scrolls inside - stackable —
false - fullBleed —
false
Visual
A full-featured code editor surface with line numbers, syntax highlighting, and (when enabled) folding and minimap. Engine-agnostic — both CodeMirror 6 and Monaco are supported and lazy-loaded.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string | '' | Current document text. |
onChange | (value: string) => void | — | Fires on every edit with the new text. |
engine | 'codemirror' | 'monaco' | codemirror | Which engine to mount. CodeMirror is lighter; Monaco offers full IDE feel. |
language | 'typescript' | 'javascript' | 'json' | 'python' | 'html' | 'css' | 'markdown' | 'plaintext' | plaintext | Language for syntax highlighting and language services. |
theme | 'light' | 'dark' | 'auto' | auto | auto follows document.documentElement.dataset.theme and updates live. |
readOnly | boolean | false | Disable editing. |
lineNumbers | boolean | auto | On by default on desktop, off on mobile. |
wordWrap | boolean | auto | Off by default on desktop, on on mobile. |
tabSize | number | 2 | Tab width in spaces. |
fontSize | number | 14 / 13 | 14 desktop, 13 mobile by default. |
height | number | string | 360 | Numbers are treated as px. Strings pass through ('50vh', '100%'). |
autoFocus | boolean | false | Focus the editor when it mounts. |
codemirrorExtensions | unknown[] | [] | Extra CodeMirror 6 extensions appended after defaults. |
monacoOptions | Record<string, unknown> | {} | Partial Monaco IStandaloneEditorConstructionOptions, spread last. |
onMount | (info: { engine, instance }) => void | — | Fires once mounted. Receives the underlying editor instance. |
Tokens used
color.bg.surface.default, color.bg.surface.subtle, color.border.default, color.border.focus, color.text.muted, color.text.primary, color.text.action.danger, color.bg.action.danger.subtle, radius.md, font.family.mono, motion.duration.fast, motion.easing.standard
Accessibility
Notes
- Both engines provide their own keyboard accessibility (cursor movement, selection, find).
- Container shows a
:focus-withinring matching helixui’s other inputs. - Respects
prefers-reduced-motionvia the global motion stylesheet — no per-engine guard required.
Composes with
| Component | Relation | Note |
|---|---|---|
CodeBlock | alternative | Use CodeBlock for static display. |
Form | parent | Can act as a controlled form input. |
Prompt examples
These are the AI prompt → JSX mappings used by the helixui prompt DSL and integrations like Cursor / Claude Code.
editable JSON config
“editable code area for JSON”
<CodeEditor language="json" value={value} onChange={setValue} />