Skip to content

Repository files navigation

MicroLighter

CI License: MIT

A tiny, dependency-free syntax highlighter for the web. MicroLighter uses the CSS Custom Highlight API and TextMate grammars to colorize code without wrapping every token in a <span>. Your markup stays clean; the highlighting lives entirely in the highlight registry and CSS.

Usage

MicroLighter ships in three flavors so you can pick the right trade-off between convenience and control. Use the HTML-standard language-* class on <code>:

<pre><code class="language-javascript">const answer = 42;</code></pre>

MicroLighter also accepts data-language="javascript" on either <code> or its parent <pre>. The older <pre lang="javascript"> form remains supported for compatibility but is deprecated because HTML's lang attribute describes human language.

Common aliases work automatically in both the import API and auto-run bundles. Project-specific names can be supplied through the import API:

await highlightAll({
  languageAliases: {
    ecmascript: "javascript",
    shellsession: "bash"
  }
});

Alias targets must be names of shipped grammars.

Web component

The opt-in microlighter/micro-lighter-element.min.js bundle registers a <micro-lighter> custom element. A readable micro-lighter-element.js bundle is also available. The element keeps the <pre><code> content in the light DOM, so the same highlighter and theme styles work without replacing the code with token markup:

<link rel="stylesheet" href="./node_modules/microlighter/themes/github.css">
<script type="module" src="./node_modules/microlighter/micro-lighter-element.min.js"></script>

<micro-lighter language="javascript" controls="copy" line-numbers>
  <pre><code>const answer = 42;</code></pre>
</micro-lighter>

The element's language attribute takes precedence when present. Without it, the component uses the standard language-* class or data-language metadata on the nested <code> or <pre>. controls accepts comma- or space-separated control names. The copy control adds a copy button. Style it with micro-lighter::part(copy-button). Add the boolean line-numbers attribute to show a gutter that stays out of copied code.

1. Auto (drop-in)

The auto-run entry (microlighter/microlighter.js, or the minified microlighter/microlighter.min.js) runs on import: it scans the page for supported <pre> > <code> block, lazily imports only the grammars it needs, tokenizes each block, registers ranges with CSS.highlights, and re-highlights whenever a syntax-highlight event fires.

<body data-syntax-theme="github">
<link rel="stylesheet" href="./src/themes/github.css">
<script type="module" src="./src/microlighter.js"></script>

To re-highlight after dynamically adding code (e.g. in a SPA):

document.dispatchEvent(new Event("syntax-highlight"));

2. Programmatic (side-effect-free)

The default entry (microlighter) exports highlightAll() and does nothing on import, so it's tree-shakeable and safe to pull into a bundler. Call it when you're ready:

import { highlightAll } from "microlighter";

await highlightAll();

// Scope to part of the page, or use a custom selector:
await highlightAll({ root: document.querySelector("#docs") });
await highlightAll({ selector: "pre.code > code" });

The low-level tokenizer is also available on its own via microlighter/highlight.js if you want to drive grammar resolution yourself.

3. CDN / single file

microlighter/microlighter.min.js is a prebuilt, minified single-file bundle of the auto runner (~2 KiB gzip) with no separate grammar requests inlined — grammars and themes are still fetched on demand:

<script type="module" src="https://cdn.example.com/microlighter/microlighter.min.js"></script>

Themes

Themes are plain CSS files that style the highlight pseudo-elements (::highlight(keyword), ::highlight(string), etc.). Load one theme directly and set its name in data-syntax-theme on <body> or any containing element:

<body data-syntax-theme="night-owl">
<link rel="stylesheet" href="./src/themes/night-owl.css">

Highlight names flatten canonical TextMate scope terms into stable CSS identifiers. Related categories share a smaller color palette:

Theme variable Highlight categories
--syntax-comment comment, quote
--syntax-keyword keyword, storage, at-rule, doctype, important, section
--syntax-operator operator, punctuation
--syntax-string string, regexp, attribute-value, link, raw
--syntax-constant numeric, boolean, constant, symbol, character-entity, entity, anchor
--syntax-function function, decorator, animation
--syntax-type type, support
--syntax-variable variable, interpolation
--syntax-property property, key, attribute-name
--syntax-tag tag
--syntax-selector selector
--syntax-inserted inserted
--syntax-deleted deleted

Bundled themes:

  • github
  • vscode-plus
  • dracula
  • monokai
  • night-owl
  • solarized-light
  • vesper
  • min
  • cobalt2
  • tokyo-night

Languages

Grammars ship as ES modules in src/grammars/ and are loaded on demand: assembly, bash, c, cpp, csharp, css, dart, dockerfile, git-diff, go, graphql, html, java, javascript, json, kotlin, lua, markdown, objective-c, perl, php, powershell, python, r, ruby, rust, scss, sql, svelte, swift, toml, tsx, typescript, vue, yaml. tsx is a genuine grammar (TypeScript constructs plus JSX), not just an alias to typescript. objective-c reuses the shared c grammar's comments, preprocessor, and strings via external includes. assembly targets generic x86 Intel/NASM syntax. First-party common blog-oriented aliases include: js and jsxjavascript, tstypescript, sassscss, sh and shell and zshbash, ymlyaml, mdmarkdown, dockerdockerfile, pypython, rbruby, and gqlgraphql.

Build

Produce the distributable dist/ folder (copied ESM modules plus unminified and minified single-file bundles at dist/microlighter.js and dist/microlighter.min.js):

npm run build

The build prints a size report: raw / gzip / brotli for the shipped bundle, a raw / minified / gzip breakdown of every lazily-loaded grammar and theme, and an average size per category (grammars vs. themes). It fails if the bundle's gzip size exceeds the sizeLimit in package.json. Run the report on its own anytime with:

npm run size

Tests

End-to-end tests drive the demo site (docs/index.html) in a real browser (system Chrome) with Playwright and assert that highlight ranges register, the core token categories are present, and theme switching re-highlights cleanly:

npm test

npm test first rebuilds dist/ and the generated docs/microlighter/ copy so tests run against current code. The pre-commit hook updates and stages the headline gzip size on the demo homepage (docs/index.html) automatically. To update it explicitly without committing, run:

npm run docs:update-homepage-stats

This rebuilds the bundle and writes the current gzip size into the homepage. Plain npm run size is read-only and never edits the homepage.

Demo

The demo lives in docs/ as a self-contained static site. npm run build generates a git-ignored copy of the package in docs/microlighter/, which docs/index.html loads. Serve the docs/ folder over HTTP so ES module imports resolve:

npm run build   # populates docs/microlighter/
npx serve docs

Publishing to GitHub Pages

The CI workflow builds and tests the package, uploads the generated docs/ directory as a Pages artifact, and deploys it on pushes to main. This keeps the package copy out of Git while avoiding a second build or a gh-pages branch. In Settings → Pages, set the source to GitHub Actions.

Contributing

Contributions are welcome — new grammars, themes, bug fixes, and docs. See CONTRIBUTING.md for the dev setup, project layout, and how to add a grammar or theme.

Prior art

MicroLighter stands on the shoulders of a lot of existing work. The technique of highlighting code with the CSS Custom Highlight API — mapping token ranges to Highlight objects instead of wrapping every token in a <span> — is not new, and neither are TextMate grammars. What MicroLighter adds is a tiny, dependency-free implementation: it parses TextMate grammars with the browser's native RegExp (using the d flag for match indices) rather than shipping the Oniguruma WASM engine, lazily loads grammars, and flattens scopes into semantic category names derived from TextMate.

Foundations and inspiration:

  • TextMate grammars — the grammar format (scopes, begin/end, patterns, repository, include) that MicroLighter interprets. Popularized by TextMate and adopted by VS Code, which is where most of the community .tmLanguage.json grammars come from.
  • Prism.js — inspiration for styling semantic token categories directly with CSS. MicroLighter uses ::highlight(...) rather than generated token spans and names its categories from TextMate scopes.
  • Shiki — the canonical TextMate-grammar-based highlighter for the web (backed by VS Code's Oniguruma tokenizer). MicroLighter trades Shiki's accuracy and language coverage for zero dependencies and a smaller footprint.
  • Bramus Van Damme's "Syntax Highlighting code snippets with Prism and the Custom Highlight API" — the 2024 write-up that popularized using the Custom Highlight API for syntax highlighting.

Similar projects worth knowing about:

  • textmate-highlighter — TextMate grammars + VS Code themes with a CSS Custom Highlights render target (uses Oniguruma).
  • syntax-highlight-element — a web component that pairs Prism.js with the Custom Highlight API.
  • shiki-highlight-api — renders Shiki tokens through the Custom Highlight API.
  • syntaxp — a minimal auto-detecting highlighter over the Custom Highlight API.

License

MIT © Dave Rupert

Releases

Packages

Contributors

Languages