Business & Finance
Introducing MicroLighter
Key Points
I made a smol client-side syntax highlighter that uses the CSS Custom Highlights API called MicroLighter. I’ll talk about why I made it in a bit but first I wanted to… ahem… highlight… some of the features. - Zero-dependencies - ~2kb minifed+gzipped - Uses CSS ::highlight(token-name) , not spans - Leverages Textmate’s language grammars - Human-readable light-dark() themes - All languages/grammars loaded on-demand - Moves all non-highlight functionality to a custom element At some point I...
I made a smol client-side syntax highlighter that uses the CSS Custom Highlights API called MicroLighter. I’ll talk about why I made it in a bit but first I wanted to… ahem… highlight… some of the features.
- Zero-dependencies
- ~2kb minifed+gzipped
- Uses CSS
::highlight(token-name)
, not spans - Leverages Textmate’s language grammars
- Human-readable
light-dark()
themes - All languages/grammars loaded on-demand
- Moves all non-highlight functionality to a
custom element
At some point I broke syntax highlighting on my Jekyll blog. I’ve used a handful of syntax highlighters over the years (Highlight.js, PrismJS, Rouge, Shiki, etc, etc) and I’ve felt the trade-offs between different client-side and server-side implementations. Faced with picking another, I knew I wanted to explore Bramus’s technique of syntax highlighting with the CSS Custom Highlights API.
There’s some limitations with the CSS ::highlight()
pseudo; no italics, no bold, no font swapping – but otherwise it’s pretty cool syntax to idiomatically express “I want to highlight this token” via CSS instead of injecting spans everywhere. Using the Highlight API means I avoid any DOM mutation and the scope of the library shrinks down to: scan code blocks using regex patterns and send CSS.highlights.set(category, textRanges)
to highlight the code blocks.
I don’t need much syntax highlighting on this site. Not all posts have code and my code samples are a whole fifteen lines long at best. My struggle is that I swap languages often. I’ll do HTML, CSS, and JavaScript all in the same post. A little bash
here, a little ruby
there, some markdown
as a treat. Using so all those languages grew the complexity beyond the limits of my regex-fu, so I decided to lean on Textmate’s established collections of patterns used by VS Code. And before I knew it, my little highlighter could do almost any language.
Knowing I use different languages often, one principle I established was that all language grammars should be auto-loaded on-demand to reduce configuration and bundle size. That way you only pay for what you use.
Inspired by PrismJS’s simplified token categories I flattened down Textmate’s granular token categories to a more human-friendly set, making it easier to style. In addition to that, one enormous nit-pick I have with codeblock styling is that light and dark themes are separate entities, so I merged them into one theme using light-dark()
.
The last big opinion I baked in was that I wanted the syntax highlighter to do one job: infer language and highlight code in that language. With that a guideline, I moved all extra functionality (like line-numbers, etc) over to a web component. The vanilla web component adds about ~1 KiB in size, but co-locating UI into a UI primitive like native custom elements feels right and the ShadowDOM encapsulation makes it easy to separate the code from the presentational UI.
Obviously, I’m a web component pervert but it feels like a great separation of concerns versus trying to cram everything into core library.
Try it out
To get started on your site, I’d use the self-initializing minified bundle, but I’m also shipping ESM and a web component.
npm install microlighter
Like I said above, not all my posts have syntax highlighting, so I even wait to import the script unless I know there’s a page with code on it.
if(document.querySelector('pre>code').length) {
import('path/to/microlighter/microlighter.min.js');
}
You can use the ESM version if you’re going to do something fancy yourself:
import { highlightAll } from 'microlighter'
highlightAll({
selector: 'pre.onlyTheseGetHighlights'
})
And you can also use the web component if you want those extra features I talked about:
Code goes here
Web component classes are pretty extendible too so if I don’t support something you need, you can “fork” it by extending the base class and adding your own features.
Lastly, you can use one of the pre-supplied themes or roll your own. The basic structure is:
/**
* Setup semantic `--syntax-*` tokens
* @value background | foreground | comment | keyword |
* operator |string | constant | function | type | variable |
* property | tag | selector | inserted | deleted
*/
[data-syntax-theme="my-theme-name"] {
color-scheme: light dark;
/* Code block tokens */
--syntax-background: light-dark(#f8f8f8, #3a3a3a);
--syntax-foreground: light-dark(#3a3a3a, #f8f8f8);
/* Highlight tokens */
--syntax-comment: light-dark(#6e7781, #8b949e);
--syntax-function: light-dark(#8250df, #d2a8ff);
/* ...etc... */
}
[data-syntax-theme="my-theme-name"] pre:has(code) {
background-color:var(--syntax-background);
color:var(--syntax-foreground)
}
::highlight(comment) { color: var(--syntax-comment) }
::highlight(function) { color: var(--syntax-function) }
/* ...etc... */
And that’s MicroLighter. If you end up using it and trying it out, let me know what you think.