expressive-code

A framework-agnostic engine that renders source code as richly annotated, syntax-highlighted HTML.

Library
npm
v0.44.2
963stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
73/100Good
Development Activity72
Maintenance76
Community48
Maturity56
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
85/100Excellent
Architecture88
Code Quality88
Innovation75
Learning Curve90

Expressive Code is the core engine behind the Expressive Code ecosystem, bundling the plugin architecture, Shiki-based syntax highlighting, text markers, and editor/terminal frames into one convenient package. It converts plain code blocks into a HAST tree that can be rendered to HTML, giving every annotation - highlighted lines, inline text markers, diff additions and deletions, window frames with titles - a consistent, themeable output without depending on any client-side framework.

While most users reach Expressive Code through a higher-level integration such as astro-expressive-code or rehype-expressive-code, this package is the low-level building block those integrations are built on. It exposes the ExpressiveCode class (an ExpressiveCodeEngine subclass) that wires up the default plugins - Shiki, Frames, and Text Markers - and lets integration authors call render() directly, collect the generated base/theme styles and JS modules, and hand the resulting AST to their own templating layer.

Because every annotation is implemented as a plugin hook rather than a hardcoded rendering step, Expressive Code can be extended with custom annotations (collapsible sections, line numbers, and more ship as separate official plugins) without forking the core renderer. The engine also handles theme-aware CSS generation, including automatic light/dark mode switching and WCAG contrast adjustments for syntax highlighting colors.

What You Get

  • The ExpressiveCode class, a preconfigured ExpressiveCodeEngine that bundles the Shiki, Frames, and Text Markers plugins by default
  • Shiki-powered syntax highlighting using the same TextMate grammars and themes as VS Code
  • Text marker annotations for highlighting full lines or inline ranges, including inserted/deleted diff styling
  • Editor and terminal window frames with optional tab or window-caption titles
  • Automatic generation of base styles, per-theme CSS variables, and any required client-side JS modules
  • A plugin hook system (ExpressiveCodePlugin) for adding fully custom annotations without touching the core renderer

Common Use Cases

  • Building a markdown/MDX code-block integration - authors of remark/rehype plugins or static site generators use expressive-code directly to render fenced code blocks to HTML
  • Custom documentation tooling - teams building an in-house docs pipeline call render() on code snippets and inject the returned styles and HTML into their own page templates
  • Framework integration authoring - anyone building an Expressive Code integration for a new framework starts from this package instead of reimplementing highlighting and annotation logic
  • Programmatic code-to-HTML generation - scripts and tools that need to convert source snippets into styled, shareable HTML (e.g. for blog posts, changelogs, or code-example galleries)

Under The Hood

Architecture The ExpressiveCode class (packages/expressive-code/src/index.ts) extends ExpressiveCodeEngine (packages/@expressive-code/core/src/common/engine.ts), which owns a layered render pipeline split across internal/render-group.ts, render-block.ts, and render-line.ts: block-level annotations resolve first, then each line is rendered and hast-transformed. Extensibility runs entirely through plugin hooks - ExpressiveCodePlugin objects register a hooks map (e.g. postprocessAnnotations in internal/core-plugins.ts) that the engine invokes at fixed pipeline phases, an explicit hook contract rather than subclassing. The @expressive-code/core package itself depends on no specific plugin; this bundle package composes Shiki, Frames, and Text Markers together in its constructor, prepending each unless disabled or already present in the caller’s plugin list. Because every official integration (astro-expressive-code, rehype-expressive-code, remark-expressive-code) depends on the same render pipeline and hook contract, a change to the core engine or renderer propagates to all of them at once.

Tech Stack Strict TypeScript across the monorepo (tsconfig.base.json enables strict and exactOptionalPropertyTypes). AST construction and querying goes through the unified/rehype ecosystem - hastscript, hast-util-to-html, hast-util-select, hast-util-to-text, unist-util-visit(-parents) - while theme/color processing uses @ctrl/tinycolor, culori, and postcss/postcss-nested for contrast-aware CSS variable generation. Syntax highlighting is delegated entirely to Shiki, the same TextMate-grammar engine VS Code uses. Each package builds independently via tsup (esbuild-based) across a pnpm workspace (packages/@expressive-code/*, packages/*, internal/*), with Changesets managing versioning and release notes.

Code Quality The core package alone ships 12 dedicated Vitest suites (block, engine, render-line, plugin-hooks, plugin-data, theme, css, color-transforms, i18n, line, meta-options, objects), several of which systematically probe invalid-argument handling (e.g. block.test.ts asserts constructor throws across non-string/number/object inputs) rather than only covering happy paths. ESLint, Prettier, and markdownlint all run in CI (.github/workflows/ci.yml) alongside the test suite. Error handling favors explicit thrown Errors with migration guidance over silent fallbacks - the textMarkers/frames config-shape checks in expressive-code’s constructor are one example. No gaps in test coverage were evident during this review.

What Makes It Unique Rather than being a syntax highlighter with markup bolted on, the package’s real contribution is a uniform hook-driven annotation model: highlighted lines, inline text markers, diffs, editor frames, and even indentation handling (the built-in “Indent wrapper” core plugin) are all implemented as ExpressiveCodeAnnotation objects processed through the identical hast-transformation contract that third-party plugins (line numbers, collapsible sections) use. Combined with automatic minimum-contrast color adjustment across multiple simultaneous themes and framework-agnostic HAST output, it addresses a narrower but genuinely underserved problem - consistent, accessible, multi-theme code annotation - that general-purpose highlighters like Shiki or Prism leave entirely to the consuming application.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search