npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

yace

v1.1.1

Published

tiny text editor with plugins

Readme

Yace

npm Coverage Status bundlephobia

Yace is a tiny, extensible code editor component for cases where a plain <textarea> is not enough and Monaco or CodeMirror would be overkill. Under 2KB gzipped. Zero dependencies. Framework agnostic.

See live examples

When Yace fits

Use Yace for embedded code samples, documentation playgrounds, Markdown inputs, prompt editors, token visualizers, and custom text experiences. It gives you syntax highlighting and editing hooks without bringing an IDE into the page.

Large documents, multicursor editing, rich text, decorations, and SSR are out of scope by design: they need a document model rather than a textarea overlay. Yace deliberately stays smaller and gives you the pieces to build the editor you actually need.

Quick start

Install Yace from npm:

npm install yace

Add a mount point:

<div id="editor"></div>

Create an editor with syntax highlighting and familiar editing behavior:

import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import { history, preserveIndent, tab } from "yace/plugins";

const editor = new Yace("#editor", {
  value: "const answer = 42;",
  lineNumbers: true,
  highlighters: [code()],
  plugins: [history(), tab(), preserveIndent()],
});

editor.onUpdate((value) => console.log(value));

Keep history() first: it checkpoints the textarea state before later plugins transform it.

Yace is ESM-only. You can also load the core directly from a CDN without a build step:

import { Yace } from "https://unpkg.com/yace?module";

How it works

A transparent <textarea> sits over a highlighted <pre>. The textarea owns input, caret, selection, IME, and accessibility. The <pre> paints the same text underneath it.

The overlay pattern is established. Yace keeps the implementation small and turns the two extension points into explicit pipelines:

  • Plugins receive the textarea state and an input event, then return the next text and selection
  • Highlighters receive the current value and return the HTML rendered by the <pre>; chain them to tokenize first and decorate the result afterward

The core ships no required highlighter or editing behavior. Start with the native textarea and add only what the editor needs.

Examples

Constructor options

new Yace(target, options) takes a CSS selector or DOM Node. Every option is optional:

new Yace(target, {
  value: "",           // sets the initial text
  highlighters: [],    // defines the rendering pipeline
  plugins: [],         // defines the editing pipeline, reduced from left to right
  lineNumbers: false,  // adds a line-number gutter
  styles: {},          // applies inline styles to the editor root, such as `{ fontSize: "20px" }`
});

Instance API

The DOM Yace built, and the methods to drive it:

  • editor.value holds the current text
  • editor.textarea exposes the real <textarea> for focus, readOnly, spellcheck, and other native features
  • editor.root exposes the mount element
  • editor.pre exposes the highlighted <pre> layer
  • editor.onUpdate((value: string) => void) registers a callback for edits
  • editor.update({ value?, selectionStart?, selectionEnd? }) updates the text or selection; a value-only update preserves the current selection
  • editor.updateOptions({ value?, lineNumbers?, highlighters?, plugins?, styles? }) changes constructor options on a live editor
  • editor.destroy() removes listeners and nodes and restores the container; later update() calls are silent no-ops

Plugins

A plugin receives the textarea state and DOM event, then returns the next state or nothing:

type Plugin = (
  props: TextareaProps,
  event: Event,
) => Partial<TextareaProps> | void;

interface TextareaProps {
  value: string;
  selectionStart: number;
  selectionEnd: number;
}

Yace runs plugins from left to right on keydown, input, and compositionend. Each plugin sees the previous plugin's result.

import { Yace } from "yace";

const upperCase = ({ value }) => ({ value: value.toUpperCase() });

new Yace("#editor", { plugins: [upperCase] });

First-party plugins are available from yace/plugins or individual subpaths such as yace/plugins/tab:

  • history({ limit = 300, coalesceMs = 300 }) adds undo and redo, caps stored states with limit, and merges nearby edits using coalesceMs; it must be first in the pipeline
  • tab(tabCharacter = " ") indents or outdents the selection with Tab and Shift+Tab
  • preserveIndent() carries the current line's indentation to the next line
  • cutLine(predicate?) cuts the selection or current line on Ctrl/Cmd+X; predicate can override the trigger
  • autoClose(pairs?) inserts matching brackets, wraps selections, steps over existing closing characters, and removes empty pairs together
  • toggleComment(prefix = "// ", predicate?) toggles line comments on the current line or selection; blank lines are skipped
  • isKey(shortcut, event) matches shortcuts with Ctrl/Cmd normalization and a physical-code fallback for non-Latin keyboard layouts

Highlighting

A highlighter turns text into HTML. Yace assigns the result to innerHTML, so the first highlighter must escape user text. Returning raw input is an XSS vector.

type Highlighter = (value: string, context?: { html: boolean }) => string;

Highlighters run as a pipeline. The first stage receives raw text with context.html === false. Later stages receive the previous stage's HTML with context.html === true, so they must preserve tags instead of escaping the whole input again. An empty pipeline falls back to Yace's escaping highlighter.

import { Yace } from "yace";
import { code } from "yace/highlighters/code";
import { shimmer } from "yace/highlighters/shimmer";

new Yace("#editor", {
  highlighters: [code(), shimmer({ words: ["TODO"] })],
});

Highlighters must keep glyph advance widths constant or the painted text will drift away from the native caret. Bold and italic are safe when every face in the font stack is strictly monospace.

First-party highlighters are available from yace/highlighters or individual subpaths such as yace/highlighters/code:

  • code(extraRules?) is an extensible tokenizer that emits yace-tok classes without imposing colors. Provides approximate highlighting, without a language parser. For language-specific highlighting, see the highlight.js and PrismJS examples.
  • markdown() highlights Markdown with the vendored mdhl and emits mdhl-* classes; place it first or use it alone
  • sliceGlitch(options?) splits lines into displaced RGB slices and exposes --yace-slice-a and --yace-slice-b for theming
  • shimmer(options?) sweeps a light band over text and exposes --yace-shimmer-base and --yace-shimmer-band for theming

sliceGlitch and shimmer also accept a words array to decorate only matching words. They inject their animation CSS at runtime; code and markdown emit classes only, so the consumer supplies token colors.

Browser support

Yace supports evergreen Chromium, Firefox, WebKit, and current mobile browsers. It is client-only and needs the DOM. IE and SSR are not supported.

Gotchas

  • With a bundler, yace/plugins and yace/highlighters tree-shake normally; with native browser modules, prefer exact paths such as yace/plugins/tab to avoid fetching every sibling
  • require("yace") on Node 22+ returns the ESM module namespace, so use const { Yace } = require("yace"); the same applies to every subpath
  • TypeScript consumers compiling to CommonJS need "module": "nodenext" and TypeScript 5.8 or newer; node16 rejects require of an ESM package
  • Jest's default CommonJS runtime cannot load ESM; use Vitest or Jest's ESM mode
  • Token styles may use bold or italic only when the complete font stack is strictly monospace; proportional fallback faces shift the painted layer away from the caret

Development

Install dependencies and start the landing page on http://localhost:5714:

npm install
npm run dev

Verify the library before submitting a change:

npm test
npm run typecheck
npm run prettier:check
npm run test:e2e
npm run attw

License

MIT