v2.0.0

<bmx-code-editor>

A code editor in the page: colouring for more than twenty languages, line numbers, several cursors, find and replace with regular expressions, folding, bracket matching, automatic indent and closing brackets, comment toggling, suggestions as you type, problems underlined (JSON checked as it is typed, and your own from any linter), and a million lines without slowing down. Everything runs in the browser; nothing is sent anywhere.

26 properties · 4 events · 28 methods · 6 parts

Example

Find "total" Fold everything Format

Try Ctrl+D on a word, Alt+click for another cursor, Ctrl+/ to comment, Alt+Up and Down to move lines, Ctrl+F to find, and Ctrl+Space for suggestions. In JSON, a missing comma is underlined as you type.

Problems from a linter, and suggestions of your own

The editor takes problems from any linter as diagnostics (F8 goes from one to the next) and suggestions from a function of yours as completions.

Show markup
<div class="row" style="gap: 0.5rem; flex-wrap: wrap; margin-block-end: 0.5rem; align-items: center">
  <label for="ex-code-lang">Language</label>
  <select id="ex-code-lang">
    <option value="typescript" selected>TypeScript</option>
    <option value="json">JSON</option>
    <option value="sql">SQL</option>
    <option value="python">Python</option>
    <option value="html">HTML</option>
    <option value="markdown">Markdown</option>
  </select>
  <label><input type="checkbox" id="ex-code-wrap"> Wrap lines</label>
</div>

<bmx-code-editor id="ex-code" language="typescript" label="Order service" status-bar style="--bmx-code-editor-height: 26rem"></bmx-code-editor>

<div class="row" style="margin-block-start: 0.75rem; gap: 0.5rem; flex-wrap: wrap">
  <bmx-button variant="outline" tone="neutral" id="ex-code-find">Find "total"</bmx-button>
  <bmx-button variant="outline" tone="neutral" id="ex-code-fold">Fold everything</bmx-button>
  <bmx-button variant="outline" tone="neutral" id="ex-code-format">Format</bmx-button>
</div>
<p class="note" id="ex-code-out">Try Ctrl+D on a word, Alt+click for another cursor, Ctrl+/ to comment, Alt+Up and Down to move lines, Ctrl+F to find, and Ctrl+Space for suggestions. In JSON, a missing comma is underlined as you type.</p>

<h3>Problems from a linter, and suggestions of your own</h3>

<p class="note">The editor takes problems from any linter as <code>diagnostics</code> (F8 goes from one to the next) and suggestions from a function of yours as <code>completions</code>.</p>
<bmx-code-editor id="ex-code-sql" language="sql" max-lines="10" min-lines="5" label="Query"></bmx-code-editor>

<script type="module">
  await customElements.whenDefined('bmx-code-editor');

  const samples = {
    typescript: `/**
 * Totals for an order, with tax and a discount.
 */
import { round } from './money';

export interface Line {
  readonly sku: string;
  readonly quantity: number;
  readonly price: number; // per unit, in pence
}

const TAX_RATE = 0.2;
const SKU = /^[A-Z]{3}-\\d{4}$/;

export function orderTotal(lines: readonly Line[], discount = 0): number {
  let total = 0;
  for (const line of lines) {
    if (!SKU.test(line.sku)) throw new Error(\`Unknown product: \${line.sku}\`);
    total += line.quantity * line.price;
  }
  const taxed = total * (1 + TAX_RATE);
  return round(Math.max(0, taxed - discount));
}
`,
    json: `{
  "name": "orders",
  "version": "1.7.0",
  "private": true,
  "scripts": { "build": "stencil build", "test": "vitest" }
  "keywords": ["orders", "totals"]
}
`,
    sql: `-- Customers who ordered more than three times this year
SELECT c.name, COUNT(o.id) AS orders, SUM(o.total) AS spent
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.placed_at >= DATE '2026-01-01'
GROUP BY c.name
HAVING COUNT(o.id) > 3
ORDER BY spent DESC;
`,
    python: `from dataclasses import dataclass

@dataclass
class Line:
    sku: str
    quantity: int
    price: float

def order_total(lines: list[Line], discount: float = 0.0) -> float:
    """Total with tax, less any discount."""
    total = sum(l.quantity * l.price for l in lines)
    return max(0.0, total * 1.2 - discount)
`,
    html: `<!doctype html>
<html lang="en-GB">
  <head>
    <style>
      .total { font-weight: 600; color: #1864ab; }
    </style>
  </head>
  <body>
    <p class="total">Total: <output id="t">0</output></p>
    <script>
      document.getElementById('t').value = (42).toFixed(2);
    <\/script>
  </body>
</html>
`,
    markdown: `# Release notes

Version **1.7.0** brings a \`bmx-code-editor\` and a [diff](#diff).

- Several cursors
- Find and replace

\`\`\`ts
const editor = document.querySelector('bmx-code-editor');
\`\`\`
`,
  };

  const editor = document.getElementById('ex-code');
  editor.value = samples.typescript;
  document.getElementById('ex-code-lang').addEventListener('change', e => {
    editor.language = e.target.value;
    editor.setValue(samples[e.target.value]);
  });
  document.getElementById('ex-code-wrap').addEventListener('change', e => (editor.wrap = e.target.checked));
  document.getElementById('ex-code-find').addEventListener('click', async () => {
    const n = await editor.find('total');
    document.getElementById('ex-code-out').textContent = `${n} matches. F3 goes to the next one.`;
  });
  document.getElementById('ex-code-fold').addEventListener('click', () => editor.foldAll());
  document.getElementById('ex-code-format').addEventListener('click', async () => {
    const changed = await editor.format();
    document.getElementById('ex-code-out').textContent = changed ? 'Formatted.' : 'Formatting is built in for JSON; give the editor a formatter for other languages.';
  });

  const sql = document.getElementById('ex-code-sql');
  sql.value = `SELECT name, emial\nFROM customers\nWHERE country = 'GB'\nORDER BY name;`;
  sql.diagnostics = [{ line: 1, column: 14, endColumn: 19, severity: 'warning', message: 'Unknown column "emial". Did you mean "email"?', source: 'your linter' }];
  const columns = ['email', 'name', 'country', 'created_at', 'last_order'];
  sql.completions = ({ word }) => columns.filter(c => c.startsWith(word.toLowerCase())).map(c => ({ label: c, kind: 'property', detail: 'customers' }));
</script>

Read getValue() (or bmxCodeInput's value) for the text; value is kept current shortly after typing stops. With name, the text is posted with its form.

Keyboard, as in desktop editors: arrows, Home and End, Page Up and Down, Ctrl with arrows by words; Shift selects; Ctrl+D adds the next occurrence and Ctrl+Shift+L every one; Alt+click and Ctrl+Alt+Up/Down add cursors; Ctrl+/ comments; Tab and Shift+Tab indent; Alt+Up/Down move lines and Shift+Alt+Up/Down copy them; Ctrl+Shift+K deletes them; Ctrl+F finds, Ctrl+H replaces, F3 moves between matches; Ctrl+G goes to a line; Ctrl+Shift+[ and ] fold and unfold; Ctrl+Space suggests; F8 goes to the next problem; Shift+Alt+F formats; Alt+Z wraps lines; Ctrl+S fires bmxCodeSave.

Properties

PropertyAttributeTypeDefaultDescription
autoClose auto-close boolean true Close brackets and quotes as they are opened.
autocomplete autocomplete boolean true Suggest words as they are typed (and on Ctrl+Space).
completions property only (context: BmxCodeCompletionContext) => readonly BmxCodeCompletion[] | Promise<readonly BmxCodeCompletion[]> — Your own suggestions: (context) => items, or a promise of them. Added to the language's words and the text's own.
diagnostics diagnostics readonly BmxCodeDiagnostic[] | string — Problems to underline, from a linter or a server of your own. JSON in markup.
disabled disabled boolean false Neither editable nor posted with its form.
firstLineNumber first-line-number number 1 The number of the first line (for showing part of a file).
folding folding boolean true Let sections fold away (brackets, indentation, tags, headings).
formatter property only (text: string, language: string) => string | Promise<string> — Formats the text for Shift+Alt+F and format(): (text, language) => text, or a promise of it. JSON is formatted without one.
highlightActiveLine highlight-active-line boolean true Shade the line the caret is on.
indentSize indent-size number — How many spaces one indent step is (when indenting with spaces). Read from the text when not set.
indentWith indent-with 'spaces' | 'tabs' — Indent with spaces or tabs. Read from the text when not set.
label label string — What the editor holds, said by screen readers.
language language string 'plain' The language to colour, indent and comment: a name, an extension or a file name (typescript, ts, report.sql). Plain text when unknown.
lineNumbers line-numbers boolean true Show line numbers.
maxLines max-lines number — At most this many lines tall before it scrolls.
minLines min-lines number — At least this many lines tall (with maxLines, the editor grows with its text).
name name string — The name the text is posted under with its form.
placeholder placeholder string — Shown while the editor is empty.
readonly readonly boolean false Shown and searchable, but not editable.
required required boolean false The form will not submit while the editor is empty.
statusBar status-bar boolean false Show the caret's line and column, the indent, the language and the line ending below the text.
strings strings Partial<BmxCodeEditorStrings> | string — Words the editor shows or says: any of BmxCodeEditorStrings. JSON in markup.
tabSize tab-size number 4 How wide a tab is drawn, in characters.
validate validate boolean true Check JSON as it is typed and underline the first mistake.
value value string '' The text. Set it to load one; it is kept current shortly after typing stops (getValue() is always current).
wrap wrap boolean false Wrap long lines at the editor's width.

Events

EventDetailDescription
bmxCodeChange { readonly value: string; } The text changed and the editor lost focus, as a field's change does.
bmxCodeInput BmxCodeEditorInputDetail The text changed. detail.value is the whole text, worked out when read.
bmxCodeSave { readonly value: string; } Ctrl+S (Cmd+S) was pressed. The browser's own save is prevented.
bmxCodeSelectionChange BmxCodeEditorSelectionDetail The carets or selections moved.

Methods

MethodSignatureDescription
execute execute(command: BmxCodeEditorCommand) => Promise<boolean> Run a command by name, as its key would. Returns whether it did anything.
find find(query: string, options?: { matchCase?: boolean; wholeWord?: boolean; regex?: boolean; }) => Promise<number> Find text (opening the find bar), select the first match after the caret, and return how many there are.
fold fold(line: number) => Promise<boolean> Fold the section starting at (or around) a line, from 1.
foldAll foldAll() => Promise<void>
format format() => Promise<boolean> Format the whole text with formatter (or, for JSON, without one). Returns whether it changed.
getDiagnostics getDiagnostics() => Promise<BmxCodeDiagnostic[]> The problems underlined now: yours and JSON's own.
getLanguage getLanguage() => Promise<{ name: string; label: string; }> The language in use, by name and label.
getLanguages getLanguages() => Promise<{ name: string; label: string; aliases: string[]; }[]> Every language the editor and the diff colour: names, labels and the other names that choose them.
getLine getLine(line: number) => Promise<string> One line's text (from 1).
getLineCount getLineCount() => Promise<number>
getSelectedText getSelectedText() => Promise<string> The text of the primary selection.
getSelections getSelections() => Promise<BmxCodeEditorSelection[]> Every selection (or caret), lines and columns from 1. The primary one is first.
getValue getValue() => Promise<string> The whole text, with the line endings it came with.
goToLine goToLine(line: number, column?: number) => Promise<void> Move the caret to a line (from 1), and column, and bring it into view.
offsetAt offsetAt(position: BmxCodeEditorPosition) => Promise<number> A line and column as a character offset into the text.
positionAt positionAt(offset: number) => Promise<BmxCodeEditorPosition> A character offset into the text as a line and column.
redo redo() => Promise<boolean>
registerLanguage registerLanguage(language: BmxCodeLanguage) => Promise<void> Add a language of your own (or replace a built-in one of the same name), for every code editor and code diff on the page. Its tokenize reads one line and the state the line before ended in, and returns the line's tokens and the state it ends in.
replaceAll replaceAll(query: string, replacement: string, options?: { matchCase?: boolean; wholeWord?: boolean; regex?: boolean; }) => Promise<number> Replace every match of a query; returns how many were replaced.
replaceRange replaceRange(text: string, from: BmxCodeEditorPosition, to?: BmxCodeEditorPosition) => Promise<void> Replace the text between two places (or insert at one) without moving the carets more than the edit does.
replaceSelection replaceSelection(text: string) => Promise<void> Type text at every caret (replacing any selection), as if pasted.
setFocus setFocus() => Promise<void>
setSelection setSelection(anchor: BmxCodeEditorPosition, head?: BmxCodeEditorPosition) => Promise<void> Select from anchor to head (or put the caret at anchor), and scroll it into view.
setSelections setSelections(selections: readonly BmxCodeEditorSelection[]) => Promise<void> Put a caret (or selection) at each of these.
setValue setValue(text: string, options?: { keepHistory?: boolean; }) => Promise<void> Replace the whole text. With keepHistory, the replacement can be undone; otherwise history starts again.
undo undo() => Promise<boolean>
unfold unfold(line: number) => Promise<boolean> Unfold the section starting at (or around) a line, from 1.
unfoldAll unfoldAll() => Promise<void>

CSS shadow parts

PartDescription
content the text area.
find the find and replace bar.
frame the editor's border and background.
gutter the line numbers and fold markers.
status the status bar.
suggestions the list of suggestions.

CSS custom properties

PropertyDescription
--bmx-code-editor-active-line The shade on the caret's line.
--bmx-code-editor-background Behind the text.
--bmx-code-editor-caret The caret's colour.
--bmx-code-editor-font The typeface. A fixed-width one; default the library's monospace.
--bmx-code-editor-font-size The text size. Default 0.8125rem.
--bmx-code-editor-gutter-background Behind the line numbers.
--bmx-code-editor-height The editor's height (unless max-lines sizes it to its text). Default 20rem.
--bmx-code-editor-line-height The line height, as a multiple of the text size. Default 1.55.
--bmx-code-editor-selection The selection's colour.