CodeEditor

September 2, 2026 · View on GitHub

import {
  CodeEditor,
  sql,
  sqlCompletionSource,
  keywordCompletionSource,
} from '@react-x11/components/code-editor';

<CodeEditor
  language={sql()}
  value={query}
  onChange={(ev) => setQuery(ev.value)}
  completionSources={[
    sqlCompletionSource({ users: ['id', 'name'] }),
    keywordCompletionSource(),
  ]}
  lineNumbers
  style={{ flexGrow: 1 }}
/>;

A multiline editor for code-shaped input — a SQL box, a shell one-liner, a config field, a small IDE pane.

It registers a host element, <codeeditor> (CODE_EDITOR_ELEMENT), at its own module scope. CodeEditorNode owns the text model and the pixels; the React component owns registration, the wiring from input props to the node's handlers, and the completion popup — which is plain composition over a <popup>, not part of the element.

Props

Every prop below is on CodeEditorComponentProps, which is the component's own type; CodeEditorProps is the element's half of it, without the completion and focus props.

Text

PropTypeNotes
valuestringControlled text (with onChange).
defaultValuestringUncontrolled initial text.
onChange(ev: CodeEditorEvent) => void
onSubmit(ev: CodeEditorEvent) => voidCtrl+Enter.
onSelectionChange(ev: CodeEditorEvent) => voidCaret or selection moved — what completion UIs track.
namestringField name, echoed on every event.
placeholderstring
readOnlybooleanStill navigates and copies.
disabledbooleanInert: no default action runs, and the component stops making it focusable.

Language and diagnostics

PropTypeNotes
languageLanguage | nullThe language seam. Absent or null paints plain text.
tokenStylesTokenStylesToken type → colour/weight/italic. Default LIGHT_TOKEN_STYLES.
diagnosticsreadonly Diagnostic[]Ranges to underline, LSP-shaped.

Layout and chrome

PropTypeNotes
rowsnumberPreferred height in text lines. Default 6.
tabSizenumberTab display width and indent size. Default 4.
insertSpacesbooleanIndent with spaces (default) or a real tab.
lineNumbersbooleanA gutter.
activeLinebooleanTint the caret's line.
matchBracketsbooleanHighlight the pair around the caret. Default true.

Colours: selectionColor, caretColor, gutterColor, gutterBackground, activeLineColor, matchingBracketColor, placeholderColor. Each defaults from the react-x11 theme.

Completion, focus and the handle

PropTypeNotes
completionSourcesreadonly CompletionSource[]Absent turns completion off entirely.
autoCompletebooleanQuery sources while typing. Default true when sources are given; Ctrl+Space always works.
focusableboolean
autoFocusboolean
refRef<CodeEditorHandle | null>See below.
styleStyle | Style[]

onKeyDown, onMouseDown, onFocus and onBlur pass through. User handlers run first, exactly core's ordering, so calling preventDefault() suppresses the editor's own action.

Editing

The full expected set: selection by keyboard and mouse with word and line variants, undo/redo with coalescing, X11 clipboard including PRIMARY and middle-click paste, auto-indent, Tab/Shift+Tab indentation, Ctrl+/ comment toggling, bracket matching, and diagnostics squiggles.

Escape then Tab leaves the field, so a multiline editor in a form is not a keyboard trap. Ctrl+Space asks for completions. Ctrl+Enter submits.

CodeEditorHandle

ref.current.value = 'select 1';
ref.current.replaceRange(from, to, text);
ref.current.undo();
ref.current.focus();

value is assignable, which is the DOM-input contract form libraries rely on: setting it does not fire onChange. Read-only members: name, selection, lines, language, canUndo, canRedo. Methods: selectedText(), replaceRange(), insertText(), moveCaret(), select(), selectAll(), undo(), redo(), indentSelection(dir), toggleLineComment(), copySelection(sel?), pasteFrom(sel?), scrollBy(dx, dy), caretRect(), focus(), blur().

Every length the handle takes or answers is in logical pixels, the unit a style is written in: caretRect() is the caret in the editor's own coordinates, which is exactly what a <popup anchor={{ at }}> takes, and scrollBy(dx, dy) moves by the same unit a <box>'s scrollBy does. So are the node's metrics() and measureText(), for an app laying out its own overlay against the text. On a 2x panel the editor shapes, hits and paints its text on the device grid itself; nothing an app touches changes.

CodeEditorEvent

interface CodeEditorEvent {
  type: 'change' | 'submit' | 'selectionchange';
  value: string;
  name: string | undefined;
  selection: Selection;
  target: CodeEditorHandle;
  currentTarget: CodeEditorHandle;
  nativeEvent: unknown;
  defaultPrevented: boolean;
  preventDefault(): void;
  stopPropagation(): void;
}

Shaped like a React change event on purpose — a form library that already knows ev.target.value and ev.target.name needs no adapter.

Languages

Three ways in, all through code-language:

  • Built-in, zero dependenciessql(), shell(), glsl(), javascript() ({ typescript: true } for TS), json(). Hand-written stream tokenizers on a CodeMirror-5-style line-state engine, or write your own with streamLanguage(…) in about fifty lines.
  • The CodeMirror grammar worldlezerLanguage({ name, parser }) runs any @lezer/<lang> parser. Install the grammar you want; nothing lezer ships with this package.
  • The VS Code grammar worldtextMateLanguage({ name, grammar }) runs an initialized TextMate grammar (via vscode-textmate, or shiki's core). Their tokenizer is line-state shaped too, so it drops straight in.

Completion sources

One async function each, deliberately the shape of an LSP textDocument/completion call, so a language-server client is "just another source":

type CompletionSource = (
  ctx: CompletionContext,
) => CompletionResult | null | Promise<CompletionResult | null>;

Built in: keywordCompletionSource(), wordCompletionSource(), sqlCompletionSource(schema). rankCompletions is exported so a source of your own can rank the same way.

Token themes

LIGHT_TOKEN_STYLES, DARK_TOKEN_STYLES, TOKEN_FALLBACK, tokenStyleFor(), autoTokenStyles() and isDarkBackground() are all exported. autoTokenStyles(background) is the one to reach for: it picks the palette that will actually be legible on the background the editor sits on.

Example

npm run examples:code-editor shows the three input-field use cases side by side.