<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
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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
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
| Part | Description |
|---|---|
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
| Property | Description |
|---|---|
--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. |