# expressive-code: an engine for presenting source code on the web

> Expressive Code is not a syntax highlighter, it is a rendering engine for code blocks, and the difference shows in what it can put around the code. You get the highlighting engine that VS Code uses, plus text markers that highlight a line in prose, diff highlighting, and frames that make a snippet look like it came from an editor or a terminal.

**expressive-code/expressive-code** — A text marking & annotation engine for presenting source code on the web.

- Repository: https://github.com/expressive-code/expressive-code
- Website: https://expressive-code.com/
- Stars: 967 · Forks: 50
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/expressive-code-expressive-code

## An engine, not a highlighter

The description is short and it sets the scope: a text marking and annotation engine for presenting source code on the web.

The readme expands it. Expressive Code is an engine for presenting source code on the web, aiming to make code easy to understand and visually striking. On top of accurate syntax highlighting powered by the same engine as VS Code, it allows you to annotate code blocks using text markers, diff highlighting, code editor and terminal window frames, and more.

That last phrase, the same engine as VS Code, is the part that matters technically. Syntax highlighting is a solved problem with a mature implementation, and the highlighter that ships with the most popular editor is a good one. Reusing it means the colours in your documentation match the colours in your editor, which is a small courtesy that people notice.

But highlighting is presented as the foundation rather than the feature. The features are the annotations, and the difference between a highlighter and an engine is what you can do to the block as a whole. A highlighter takes tokens and gives them classes. An engine takes a code block and a set of instructions, and produces a rendered element with a frame, markers, line numbers, collapsible sections and highlighted ranges.

The intended audience is stated plainly: no matter if you are writing a blog, a guide, or a full documentation website, Expressive Code will help you make your code examples stand out.

## Text markers, the feature that changes how you write docs

The default plugin list is short, and the text markers plugin is the one that changes how documentation gets written.

The text markers plugin allows you to add text markers to your code blocks, highlighting specific parts of the code or indicating additions or removals. The mechanism is the same idea as the marker syntax you would use in a diff, applied to prose.

The practical effect is worth spelling out. In ordinary documentation, when you say the second argument controls the timeout, the reader has to find the second argument themselves. With a marker you can point at it directly, in the sentence, in the prose, and the rendered block highlights the exact span. That is the difference between a code block and an annotated code block, and it is the thing that makes a documentation site feel authored rather than assembled.

The same plugin handles the diff case, indicating additions and removals, so you can show a before and an after in the same block without writing two blocks and explaining the difference in the text.

The other default plugins cover the fundamentals. The Shiki plugin adds syntax highlighting using the same engine as VS Code, which is the foundation the others sit on. The frames plugin surrounds your code blocks with code editor or terminal window frames, depending on the code's language, so a shell snippet appears in a terminal frame and a JavaScript snippet in an editor frame, without you choosing per block.

Two more plugins sit in the additional list. One allows marking code sections as collapsed, and one adds line numbers to your code blocks. Both are the kind of feature you would otherwise implement with a custom highlighter or a hand-written HTML block, and the fact that they are separate packages means you pay for them only if you use them.

## The package layout, and which one you actually install

The repository is a monorepo and the readme organises the packages into four groups, which is also a good guide to which one you want.

The high-level integration packages are the ones most people use. The rehype plugin processes all code blocks in markdown and MDX files with Expressive Code, and the Astro integration automatically renders code blocks in any markdown or MDX content on your site, plus it provides a component for rendering dynamic code blocks, which is the piece you need when the code is generated at run time rather than written in the file.

The default plugins are the annotation features described above: frames, Shiki highlighting and text markers, with the two additional ones for collapsible sections and line numbers.

The low-level packages are for integration authors. The bundle package gives convenient access to the core engine and all default plugins, and the core package is the engine itself. If you are writing a plugin for a framework that has no package here, that is where you start.

The plugin architecture is the connective tissue. All annotations are based on a plugin architecture that allows you to extend the functionality with your own custom annotations, and the readme is clear that the extension point is at the annotation level rather than at the rendering level. A custom annotation is a transformation of a code block, and the engine takes care of the frames, the highlighting and the markup around it.

One property is worth calling out for anyone evaluating it, and it is stated early: Expressive Code does not depend on any client-side framework, is built with performance in mind and is designed to be as lightweight as possible. The output is static markup with a stylesheet, which means the code blocks on your site cost nothing at run time.

## The monorepo tooling, and how a release happens

The root package manifest is a good illustration of a modern JavaScript monorepo, and the release script is the interesting part.

The build is a recursive package build with streaming output, so you can watch the packages compile in order. The lint step runs the linter over the repository and a separate markdown linter over the documentation. The test and coverage scripts recurse into the packages with the no-bail flag and a workspace concurrency of one, which means a failing package does not stop the others and the packages do not fight over resources.

The release is the piece that tells you how the project is run. The validate script installs, builds, tests and lints in one chain. The bump script runs the changesets versioning tool and reinstalls. And the release script is validate, then bump, then publish:

```json
"release": "pnpm validate && pnpm bump && changeset publish"
```

So no release happens without the full check having passed first, and the version numbers are produced by a tool that reads the change entries the contributors wrote, rather than by a person editing manifests. The repository has a changesets directory at the root, which is where those entries live.

There is a separate continuous integration publish path that builds, tests, lints and publishes without the validation step, and a version script for continuous integration that runs the versioning and reinstalls without the frozen lockfile.

The workspaces cover the scoped plugin packages, the other packages and an internal directory, and the toolchain is pinned: Node 18.14.1 or newer, pnpm 9 or newer, and a specific pnpm version as the package manager. One override in the manifest pins a package inside another project to the workspace version, which is how a monorepo keeps a test of a downstream integration honest.

## The project state and where to start

The licence is MIT, the default branch is main, and the project is not archived, with the last push on 2026-10-07.

The release history shows the versioned packages moving together. Version 0.44.2 was published on 2026-08-31 for the remark integration, the rehype integration and the main bundle at the same time, which is the release policy working as intended: the monorepo releases a coherent set, so a rehype integration and the core engine are never a version apart.

For a reader evaluating the project, the documentation site is where the real material is, and the readme says so plainly, pointing to it for the features, the available framework integrations and the usage instructions. There is a separate installation page, a contributing guide for the development process, and a documentation directory in the repository for the pages that live on the site.

The one practical point to carry into a decision is where the work happens. The rendering is done at build time, in the markdown or MDX pipeline, not in the browser, which is why there is no client-side framework dependency and why the output is as cheap to serve as any other static markup. If your site is built with a tool that has no integration here, the low-level core package exists for exactly that case, and the plugin architecture is the documented way to extend it.

## Conclusion

Expressive Code suits a documentation site or a blog whose code examples carry meaning that a plain highlighter cannot express, because a marker in the prose that points at a line is the thing documentation actually needs. It is a poor fit if you only want coloured text, since a highlighter alone is far less machinery, and it is a poor fit if your site is not built on a framework with an integration package. Before you install it, check which integration applies to your stack, because the rehype and Astro packages are the intended entry points, and plan for the fact that the output is generated at build time rather than in the browser.

## FAQ

### What is Expressive Code and how is it different from a syntax highlighter?

It is an engine for presenting source code on the web rather than only colouring it. On top of highlighting powered by the same engine as VS Code, it can annotate blocks with text markers, diff highlighting, editor or terminal window frames, line numbers and collapsible sections, and it can be extended with your own annotations through a plugin architecture.

### Which package should I install?

If your site uses markdown or MDX through a rehype pipeline, the rehype plugin processes all code blocks for you. If it is an Astro site, the Astro integration does the same and adds a component for dynamic code blocks. For a framework with no package here, the bundle package gives access to the core engine and the default plugins.

### Does Expressive Code need a client-side framework?

No. The readme states that it does not depend on any client-side framework, is built with performance in mind and is designed to be as lightweight as possible, because the rendering happens at build time and the output is static markup.

### How are Expressive Code releases published?

Through changesets. The release script validates by installing, building, testing and linting, then runs the changesets versioning step, then publishes. The manifest also has a separate continuous integration publish path and a version script for continuous integration that reinstalls without the frozen lockfile.

## Sources

- [expressive-code/expressive-code on GitHub](https://github.com/expressive-code/expressive-code)
- [License: MIT](https://github.com/expressive-code/expressive-code/blob/main/LICENSE)
- [Project website](https://expressive-code.com/)
- [README](https://github.com/expressive-code/expressive-code/blob/main/README.md)
- [Releases](https://github.com/expressive-code/expressive-code/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/expressive-code-expressive-code
