Skip to content

CodeEditor

CodeEditor

A single component, two engines.

EngineBundleStrengthsMobile
codemirror (default)~150 KB gzTouch-friendly, fast cold start, themeable via tokensExcellent
monaco~2 MBFull IDE feel: hover, find/replace, multi-cursor, diffWorkable

Both engines are declared as optional peer dependencies of @helixui/core. Install only what you use:

Terminal window
# CodeMirror
pnpm add codemirror @codemirror/state @codemirror/view @codemirror/commands \
@codemirror/language @codemirror/lang-javascript @codemirror/lang-json \
@codemirror/theme-one-dark
# Monaco
pnpm add monaco-editor @monaco-editor/react

The 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 vsvs-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

  • displayblock
  • widthfill
  • heightfixed:320px
  • intrinsicSizefills width, default 320px tall, scrolls inside
  • stackablefalse
  • fullBleedfalse

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

NameTypeDefaultDescription
valuestring''Current document text.
onChange(value: string) => voidFires on every edit with the new text.
engine'codemirror' | 'monaco'codemirrorWhich engine to mount. CodeMirror is lighter; Monaco offers full IDE feel.
language'typescript' | 'javascript' | 'json' | 'python' | 'html' | 'css' | 'markdown' | 'plaintext'plaintextLanguage for syntax highlighting and language services.
theme'light' | 'dark' | 'auto'autoauto follows document.documentElement.dataset.theme and updates live.
readOnlybooleanfalseDisable editing.
lineNumbersbooleanautoOn by default on desktop, off on mobile.
wordWrapbooleanautoOff by default on desktop, on on mobile.
tabSizenumber2Tab width in spaces.
fontSizenumber14 / 1314 desktop, 13 mobile by default.
heightnumber | string360Numbers are treated as px. Strings pass through ('50vh', '100%').
autoFocusbooleanfalseFocus the editor when it mounts.
codemirrorExtensionsunknown[][]Extra CodeMirror 6 extensions appended after defaults.
monacoOptionsRecord<string, unknown>{}Partial Monaco IStandaloneEditorConstructionOptions, spread last.
onMount(info: { engine, instance }) => voidFires 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-within ring matching helixui’s other inputs.
  • Respects prefers-reduced-motion via the global motion stylesheet — no per-engine guard required.

Composes with

ComponentRelationNote
CodeBlockalternativeUse CodeBlock for static display.
FormparentCan 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} />