
Yace is a tiny, extensible code editor component that upgrades a plain <textarea> into a syntax-highlighted input with configurable editing behavior.The core stays under 2KB gzipped and has zero runtime dependencies.
The code editor uses ES modules, mounts on a regular DOM node, and works with vanilla JavaScript, React, Vue, and other browser frameworks.
Editing behavior and visual rendering remain separate. Plugins handle history, indentation, bracket pairing, comments, shortcuts, and custom text transformations. Highlighters render code, Markdown, tokens, and decorative effects beneath the native input layer.
Features:
- Native textarea input with synchronized highlighted rendering.
- Optional line numbers for code and configuration fields.
- Plugin pipeline for history, indentation, brackets, comments, and shortcuts.
- Highlighter pipeline for source code, Markdown, tokens, and text effects.
- Direct DOM mounting for vanilla JavaScript and component frameworks.
- Live value, selection, plugin, highlighter, and style updates.
- TypeScript declarations and ESM package exports.
How To Use It:
Installation
Install the package with NPM
npm install yace
Import yace
<script type="importmap">
{
"imports": {
"yace": "https://esm.sh/yace@1",
"yace/": "https://esm.sh/yace@1/"
}
}
</script>
Basic Usage
The editor needs a mount element, an initial value, and any optional plugins or highlighters. The bundled code highlighter outputs token classes but leaves their colors under project control.
Add the editor container.
<div id="code-editor"></div>
Define colors for the token classes used by the bundled code highlighter.
.yace-tok--kw {
color: #d73a49;
}
.yace-tok--str {
color: #032f62;
}
.yace-tok--com {
color: #6a737d;
}
.yace-tok--num {
color: #005cc5;
}
.yace-tok--punc {
color: #586069;
}
Create the editor and listen for text changes. Keep history() at the start of the plugin array so it records the textarea state before later plugins modify it.
import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import {
history,
preserveIndent,
tab
} from "yace/plugins";
const initialCode = `const product = {
sku: "A-104",
stock: 18
};`;
const editor = new Yace("#code-editor", {
value: initialCode,
lineNumbers: true,
highlighters: [code()],
plugins: [
history(),
tab(),
preserveIndent()
]
});
editor.onUpdate((value) => {
console.log(value);
});
Constructor Options
Every constructor option is optional. The first argument accepts a CSS selector or DOM node.
value(string): Sets the initial editor text.highlighters(Highlighter[]): Defines the rendering pipeline.plugins(Plugin[]): Defines the editing pipeline and runs each plugin from left to right.lineNumbers(boolean): Displays a line-number gutter. The default value isfalse.styles(object): Applies inline style properties to the editor root.
const editor = new Yace("#code-editor", {
value: "export const status = 'ready';",
lineNumbers: true,
highlighters: [code()],
styles: {
fontSize: "16px"
}
});
Editing Plugins
Plugins receive the current textarea value and selection with the originating DOM event. Each plugin returns a partial textarea state or leaves the state unchanged.
history() belongs first when the editor uses multiple plugins.
history({ limit = 300, coalesceMs = 300 }): Adds undo and redo, limits stored states, and combines nearby edits.tab(tabCharacter = " "): Indents or outdents the current line or selection with Tab and Shift+Tab.preserveIndent(): Copies the current line indentation to the next line.cutLine(predicate?): Cuts the current selection or line through Ctrl/Cmd+X.autoClose(pairs?): Inserts closing characters, wraps selections, skips existing closers, and removes empty pairs together.toggleComment(prefix = "// ", predicate?): Adds or removes line-comment prefixes across the current line or selection.isKey(shortcut, event): Matches keyboard shortcuts with Ctrl/Cmd normalization and physical key-code support.
import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import {
autoClose,
history,
preserveIndent,
tab,
toggleComment
} from "yace/plugins";
const editor = new Yace("#code-editor", {
highlighters: [code()],
plugins: [
history({
limit: 150,
coalesceMs: 250
}),
tab(" "),
preserveIndent(),
autoClose(),
toggleComment("// ")
]
});
Create a Custom Plugin
A custom plugin receives the textarea state and the current event. It can return a new value, selection positions, or both.
The following plugin replaces tab characters with two spaces after an input event.
import { Yace } from "yace";
const normalizeTabs = ({ value, selectionStart }, event) => {
if (event.type !== "input" || !value.includes("\t")) {
return;
}
const textBeforeCaret = value.slice(0, selectionStart);
const removedTabs = (textBeforeCaret.match(/\t/g) || []).length;
const normalizedValue = value.replace(/\t/g, " ");
return {
value: normalizedValue,
selectionStart: selectionStart + removedTabs,
selectionEnd: selectionStart + removedTabs
};
};
new Yace("#code-editor", {
plugins: [normalizeTabs]
});
Highlighters
Highlighters convert the current value into HTML for the synchronized <pre> layer. They run from left to right.
The first stage receives plain text and must escape user-controlled content. Later stages receive HTML from the previous stage and must retain its existing tags.
code(extraRules?): Tokenizes source code and emitsyace-tokclasses. Custom rules run before the built-in rules.markdown(): Highlights Markdown and emitsmdhl-*classes.sliceGlitch(options?): Splits text into displaced RGB slices.shimmer(options?): Adds a moving light band across text.words: LimitssliceGlitch()orshimmer()to matching words.
The animation highlighters expose four theme variables.
--yace-slice-a: Controls the first displaced slice color.--yace-slice-b: Controls the second displaced slice color.--yace-shimmer-base: Controls the base shimmer color.--yace-shimmer-band: Controls the moving shimmer band.
Place an escaping highlighter such as code() first. Decorative highlighters belong later in the pipeline.
import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import { shimmer } from "yace/highlighters/shimmer";
new Yace("#code-editor", {
value: `// TODO: validate the response
const response = await fetch("/api/items");`,
highlighters: [
code(),
shimmer({
words: ["TODO"]
})
]
});
-root {
--yace-shimmer-base: #b42318;
--yace-shimmer-band: #fdb022;
}
Build a Markdown Editor
The Markdown highlighter belongs first in the rendering pipeline. It returns classes for headings, emphasis, links, lists, inline code, and fenced code.
Add the colors before creating the editor.
.mdhl-heading {
color: #8250df;
}
.mdhl-strong,
.mdhl-em {
color: #bc4c00;
}
.mdhl-inlineCode,
.mdhl-codeInFences {
color: #0550ae;
}
.mdhl-link {
color: #0969da;
}
.mdhl-bullet {
color: #cf222e;
}
Initialize the field with Markdown highlighting and common editing behavior.
import { Yace } from "yace";
import { markdown } from "yace/highlighters/markdown";
import {
history,
preserveIndent,
tab
} from "yace/plugins";
const markdownEditor = new Yace("#markdown-editor", {
value: `# Release Notes
The new build includes **faster search** and improved \`API\` examples.
- Updated navigation
- New keyboard shortcuts`,
highlighters: [markdown()],
plugins: [
history(),
tab(),
preserveIndent()
]
});
Update the Editor
The instance exposes the current value, native textarea, rendered layer, root element, update methods, and cleanup method.
// Read the current plain-text value.
const source = editor.value;
// Access native textarea features.
editor.textarea.readOnly = true;
editor.textarea.spellcheck = false;
editor.textarea.focus();
// Access the mount element and highlighted layer.
const editorRoot = editor.root;
const highlightedOutput = editor.pre;
// Listen for edits.
editor.onUpdate((value) => {
localStorage.setItem("draft-code", value);
});
// Replace text and set a selection range.
editor.update({
value: "const total = price * quantity;",
selectionStart: 6,
selectionEnd: 11
});
// A value-only update retains the current selection.
editor.update({
value: "const total = unitPrice * quantity;"
});
// Change options on the active editor.
editor.updateOptions({
lineNumbers: false,
styles: {
fontSize: "15px"
}
});
// Remove listeners and generated nodes.
editor.destroy();
React Integration
Yace accepts a DOM node, which fits React refs and similar lifecycle systems. Create the editor after the component mounts and call destroy() during cleanup.
import { useEffect, useRef } from "react";
import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import { history, tab } from "yace/plugins";
function CodeField({ initialValue, onChange }) {
const editorHost = useRef(null);
useEffect(() => {
const editor = new Yace(editorHost.current, {
value: initialValue,
lineNumbers: true,
highlighters: [code()],
plugins: [history(), tab()]
});
editor.onUpdate(onChange);
return () => {
editor.destroy();
};
}, [initialValue, onChange]);
return <div ref={editorHost} />;
}
Alternatives:
- WYSIWYG Markdown Editor for Mobile & Desktop Apps – OverType
- Simple Markdown Editor with Real-Time Preview and Customizable Toolbar
- Lightweight Powerful Text Editor With JavaScript
- Tiny & Fast JavaScript Syntax Highlighting Library – Speed Highlight







