Tiny Code Editor with Plugins and Syntax Highlighting – Yace

Category: Javascript , Recommended | July 30, 2026
Authorpetersolopov
Last UpdateJuly 30, 2026
LicenseMIT
Views20 views
Tiny Code Editor with Plugins and Syntax Highlighting – Yace

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 is false.
  • 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 emits yace-tok classes. Custom rules run before the built-in rules.
  • markdown(): Highlights Markdown and emits mdhl-* classes.
  • sliceGlitch(options?): Splits text into displaced RGB slices.
  • shimmer(options?): Adds a moving light band across text.
  • words: Limits sliceGlitch() or shimmer() 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:

You Might Be Interested In:


Leave a Reply