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.
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.
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.
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"));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.
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 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:
githubvscode-plusdraculamonokainight-owlsolarized-lightvespermincobalt2tokyo-night
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 jsx → javascript, ts → typescript, sass → scss, sh and
shell and zsh → bash, yml → yaml, md → markdown, docker →
dockerfile, py → python, rb → ruby, and gql → graphql.
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 buildThe 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 sizeEnd-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 testnpm 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-statsThis rebuilds the bundle and writes the current gzip size into the homepage. Plain
npm run size is read-only and never edits the homepage.
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 docsThe 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.
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.
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.jsongrammars 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.
MIT © Dave Rupert