MicroLighter's feature list says 37 languages and 10 themes, and the lists hold more
A zero-dep syntax highlighter that uses the CSS Highlights API
At a glance
- What is it?
- A two kilobyte highlighter that paints code with the CSS Custom Highlight API and TextMate grammars instead of wrapping tokens in spans. The size budget is enforced in the manifest, the build is a shell script wired into three hooks, and the headline counts have drifted from the lists.
- Who is it for?
- MicroLighter is for the case where a documentation site or blog is paying kilobytes for highlighting and does not need line-by-line markup in the DOM. The approach is sound: painting ranges through the browser's own highlight API means the text stays plain text, so a code block stays selectable, editable and copyable, and a re-highlight is one dispatched event.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 22 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The headline counts have drifted from the lists below them
The feature list at the top gives six numbers and a few capability claims. Two of the numbers disagree with what follows.
It says thirty-seven languages. The grammar list that appears later enumerates forty, and counting them is quick: the list runs from assembly through yaml and includes both component languages that are easy to forget, such as tsx and heex, and the two template languages.
It says ten bundled themes. The theme list below it names eleven, running from a light minimal theme through the familiar dark ones to an editor-native light theme.
Both counts are undercounts, which is the harmless direction. The problem is structural rather than numerical: the summary line and the lists are maintained separately, so they will keep drifting in whichever direction the editor happens to add things.
For a user the lists are what matter, and they are complete enough to plan against. The bundled themes in particular are a better list than most, covering a minimal light option, a native editor light option, and several popular dark palettes.
The grammars are described as modules that load on demand, so shipping forty of them does not mean paying for forty.
The two kilobyte claim is enforced, not asserted
Most projects that claim to be small have no way to notice when they stop being small. This one does.
The package manifest carries a size limit configuration that names one file and one number: the minified auto runner, capped at two thousand one hundred and fifty bytes. That is the same file the CDN example loads, so the budget is applied to the artefact a visitor actually downloads.
There is also a script that reports the current size, and a second script that runs the build, takes the measurement, and writes it back into the homepage copy. So the number on the project page is generated from the artefact rather than typed by hand, and the figure quoted in the readme, about two kilobytes compressed, sits just under the enforced ceiling.
Two details make the claim credible rather than decorative. The limit is expressed in the manifest, so a build that breaches it fails in the same place any other manifest problem would. And the file it applies to is the self-executing bundle, not a library entry, which is the harder one to keep small because it has to include the scanner and the grammar loader.
What is not budgeted is the grammar set or the themes, which load on demand and are counted separately.
The build is a shell script, and it runs at three points
The build is not a JavaScript tool. It is a script file at the repository root, invoked directly by three different package hooks: the build hook, the hook that runs before tests, and the hook that runs before packing.
Running it three times is sensible, since it guarantees you never test or publish a stale bundle. Invoking it as a bare script has a cost, which is that the build assumes a POSIX shell. On Windows, where the package runner uses a different shell by default, a plain install from source will not build, and the failure will look like a missing script rather than a platform problem.
The fourth hook is worth calling out separately, because it is the one that reaches outside the project. The prepare hook runs a git configuration command that points the repository's hooks path at a directory inside the project. Package managers run prepare hooks on installation from a git reference, so installing this project by git reference into your own repository will rewrite that repository's hooks path.
That is a well-known trap in the JavaScript ecosystem rather than a mistake unique to this project, and it is the kind of thing a contributor discovers locally and never encounters as a consumer. Worth knowing before you point a dependency at a git reference.
Two test runners, and nothing that installs their browsers
The test command is a chain of two suites run by two different runners:
"pretest": "./build.sh",
"test": "node --test test/*.spec.js && playwright test"The first is the built-in test runner against files matching a naming pattern in the test directory, which needs nothing installed beyond Node. The second is a browser driver, with its configuration at the repository root, and it is what actually verifies the thing this library does, since the whole product is a browser API painting text ranges.
That ordering is right. Unit tests catch the option plumbing, and the browser suite is the one that would catch a regression in the highlight ranges themselves.
The gap is provisioning. The browser driver is a development dependency and nothing in the scripts installs the browsers it wants. On a fresh clone, the unit suite passes and the browser suite fails on a missing binary rather than on a real defect, which is a confusing first experience for a contributor. There is a contributing guide, so the step may well be written down there, but it is not in the scripts.
A contributor also gets a Node version file and a separate type-check script, and the manifest declares support back to Node eighteen.
Highlighting without markup, which is why blocks stay editable
The design decision is in the first paragraph: this highlights code without adding an element around every token. Everything else follows from that.
The mechanism is the browser's own custom highlight API, which takes ranges over existing text nodes and paints them through pseudo-elements defined in CSS. A theme therefore styles ranges, not elements, and the theme file is a list of custom properties mapping thirteen token categories to colours, covering comments, keywords, operators, strings, constants, functions, types, variables, properties, tags, selectors, and inserted or deleted text.
Because the DOM is untouched, a code block stays plain text. That is what makes the editable code feature work: the readme shows a custom element that calls the highlighter again after each change, and the text a user types is still the same text nodes the highlighter walks. The same property is why the line-number feature in the web component is described as adding a gutter without changing copied code.
Re-highlighting after a change is a single dispatched event, and the readme notes it can bubble from a code block or any parent, so a single listener highights the page rather than one per block.
The copy button and the line numbers live in the optional web component build, styled through shadow parts, so the main bundle carries neither.
The recommended language marker argues against a common convention
There are three ways to tell the highlighter what language a block is, and the project tells you to stop using one of them.
The recommended form is a class on the code element, named with a language prefix. A data attribute on either the pre or the code element also works. And the standard HTML language attribute on the pre element is supported for compatibility, but the readme says explicitly to avoid it in new code, with the reason given: the HTML language attribute should describe a human language, not a programming language.
That is a correct point about the standard, and it is advice that runs against the grain, because plenty of static site generators and documentation tools emit exactly that form by default. Anyone adopting this highlighter into an existing site is likely to have blocks marked that way already, and the compatibility support is what keeps them working.
Beyond the three forms there is an alias layer. Ten common short forms are mapped automatically, covering the usual suspects for JavaScript, TypeScript, shell, YAML, Markdown, Sass, Dockerfile, Python, Ruby and GraphQL. Custom aliases can be passed as an option, with one restriction stated clearly: a custom alias has to point at a bundled grammar, so the alias table cannot be used to load a grammar you supply yourself.
Three entry points, and exactly four files with side effects
The library has three ways in, and the distinction between them is what keeps the bundle small.
The programmatic entry exports the scan function and does nothing on import. The auto runner highlights the page as soon as the module loads, which is what the CDN example and the no-build path use. The web component registers a custom element on import and provides the copy and line-number controls.
The manifest declares side effects for four files and no others: the auto runner and the web component, each in a plain and a minified build. That is the entire list of modules a bundler must not drop, and it means an application that only calls the function pays for one entry and can tree-shake the other two.
The exports map allows any file under the built directory to be imported by path, which is how the theme stylesheets and the individual grammar modules are reached. The published files list is a single directory, so nothing from the source tree, the tests or the documentation ships.
The one metadata mismatch is small: the manifest's homepage field points at the repository readme rather than at the demo site, which the project's own metadata elsewhere names as the rendered example.
Editorial conclusion
MicroLighter is for the case where a documentation site or blog is paying kilobytes for highlighting and does not need line-by-line markup in the DOM. The approach is sound: painting ranges through the browser's own highlight API means the text stays plain text, so a code block stays selectable, editable and copyable, and a re-highlight is one dispatched event. Two things to weigh. The highlighter depends on a browser API that is recent, so check your support floor before shipping it, and the copy and line-number affordances live in a separate web component build rather than in the main one. Also note that the size claim is enforced mechanically while the language and theme counts are not, so trust the enumerated lists rather than the summary line.
Frequently asked questions
What is MicroLighter?
A tiny, dependency-free syntax highlighter for the web built on the CSS Custom Highlight API and TextMate grammars. It highlights code without wrapping every token in an element, so the markup stays clean and code blocks can remain editable. Grammars load on demand and themes are plain CSS custom properties.
How many languages and themes does MicroLighter ship?
The feature list says 37 languages and 10 themes, while the enumerated lists further down name more than that, counting forty bundled grammars and eleven bundled themes. The grammars are modules loaded on demand, so shipping forty of them does not add forty to the bundle.
How do I highlight code automatically with MicroLighter?
Import the auto runner as a module and it highlights every supported code block as soon as it loads, with no call required. To highlight again after adding or changing code, dispatch a syntax-highlight event on the document; the event can also bubble up from a code block or one of its parents.
Does MicroLighter support TypeScript?
Yes. Declarations ship with the package, including for the grammar and auto-run entry points, and they are generated from the JSDoc types in the source directory so the source and the published types cannot drift. Importing the web component entry also registers the element in the global tag name map so selectors return a typed element.
What are MicroLighter's size and runtime requirements?
About two kilobytes compressed, enforced by a size limit of 2150 bytes on the minified auto runner in the package manifest, with a script that measures the bundle and writes the figure back into the homepage. There are no runtime dependencies, the manifest declares Node 18 or newer for tooling, and the highlighter itself needs a browser with the CSS Custom Highlight API.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/davatron5000-microlighter)