Skip to content

Repository files navigation

speed-highlight

NPM VersionNPM Downloads

A tiny, fast, simple syntax highlighter for the web and the terminal in JavaScript

  • Tiny (~1.5 kB gzipped core, ~1 kB gzipped per language)
  • Fast (generally outperforms Prism and highlight.js, see the benchmark)
  • Simple (zero dependencies)

Playground

Screenshot

Quick start

npm i @speed-highlight/core

In a terminal, print the highlighted string:

import{highlightANSI}from'@speed-highlight/core';importthemefrom'@speed-highlight/core/themes/default.js';console.log(awaithighlightANSI('console.log("hello")','js',theme));

In a component, highlight a string and render it:

import{useEffect,useState}from'react';import{highlightHTML}from'@speed-highlight/core';import'@speed-highlight/core/themes/default.css';exportfunctionCode({ code, lang }){const[html,setHtml]=useState('');useEffect(()=>{// highlightHTML is async (languages load on first use), skip stale resultsletstale=false;highlightHTML(code,lang).then(result=>{if(!stale)setHtml(result);});return()=>{stale=true;};},[code,lang]);return<divclassName={`shj-lang-${lang} shj-block`}dangerouslySetInnerHTML={{__html: html}}/>;}

How it works

The tokenizer runs a language's regex rules over your code and emits typed tokens (kwd, str, cmnt, ...). On the web each token becomes a <span class="shj-syn-kwd">; in the terminal it becomes an ANSI escape code. A theme is just CSS (or a token-to-escape map) coloring those names, which is why themes are under 1 kB and writing your own is a few lines.

Comparison

Highlighters trade size for grammar fidelity: TextMate engines (Shiki, starry-night) are the most faithful and heaviest, mature regex engines (highlight.js, Prism) sit in the middle, lightweight regex tokenizers (speed-highlight, sugar-high) are the smallest and approximate on exotic syntax.

Core (gzip)Per language (gzip)LanguagesGrammar modelTerminalStatusChoose it for
speed-highlight1.5 kB0.07–1.6 kB~30lightweight regex✅ built in✅ v2 (this repo)runtime highlighting where size and startup matter
sugar-high1.7 kB0.18–2.9 kB25lightweight regex✅ v2.0.0CSS-variable theming, JSX/TSX-aware JavaScript
Prism3.1 kB0.3–3 kB~290mature regex⚠️ v1.30.0, frozen since Mar 2025 (v2 rewrite)its plugin ecosystem
highlight.js8.3 kB0.3–2.5 kB~190mature regex❌ (via wrappers)✅ v11.11 (Jun 2026)broad auto-detection and rare languages
Shiki35 kB + engine: 145 kB WASM or 20 kB JS5–16 kB~220TextMate (VS Code)✅ via @shikijs/cli✅ v4.4, very activehighest fidelity (the same grammars as VS Code); zero client JS when run at build time
starry-night185 kB incl. WASM3–25 kB600+TextMate (GitHub)✅ activeGitHub-identical rendering in Node

Sizes are min+gzip, measured from the installed packages at the versions shown (per-language = range over the benchmark corpus; starry-night figures from its own README). Wrappers reuse these engines and inherit their numbers: lowlight/refractor wrap highlight.js/Prism for virtual DOMs, rehype-pretty-code and bright wrap Shiki, and the terminal-only emphasize and cli-highlight wrap highlight.js grammars into ANSI. Editors (CodeMirror, Monaco, tree-sitter) are a different category.

If you highlight at build time and bytes do not matter, use Shiki. speed-highlight's case is the opposite one: highlighting at runtime, where the entire library with all 34 grammars bundled into one file gzips to 9.0 kB, barely more than highlight.js's core alone, before it has loaded a single grammar.

Web usage

In a component

Frameworks own their DOM, so highlight the string and render it, as in the quick start (mutating a mounted node with highlightElement gets wiped on the next render). The output is HTML-escaped (&, <, >), safe to inject even for untrusted code; the shj-lang-* and shj-block classes hook it into the theme. The same pattern works in Vue, Svelte, and Angular; ready-made components are in #85.

On a plain page

Mark code blocks with a shj-lang-* class and call highlightAll once:

<divclass="shj-lang-js">console.log('hello')</div><codeclass="shj-lang-js">inline code</code><scripttype="module">import{highlightAll}from'@speed-highlight/core';highlightAll();</script>

Blocks are a single <div> instead of <pre><code> so the line-number gutter can be laid out inside; the shj-lang- prefix avoids colliding with Prism's language-* during a migration.

For per-element control use highlightElement. It renders a code element inline and anything else as a block, accepts block as an override, and sets data-lang so a theme can render a language header with content: attr(data-lang):

import{highlightElement}from'@speed-highlight/core';awaithighlightElement(element,'js',{showLineNumbers: true});

Detect the language

Detection is a separate ~1 kB import so the core stays small. It recognizes about 20 common languages and returns 'plain' when unsure:

import{highlightElement}from'@speed-highlight/core';import{detectLanguage}from'@speed-highlight/core/detect';element.textContent=code;awaithighlightElement(element,detectLanguage(code));

Control loading and bundling

Languages load lazily through a loader. Replace it with setLoader to add custom languages or restrict what your bundler includes; a name the loader cannot resolve renders as plain text:

import{setLoader,defaultLoader}from'@speed-highlight/core';// add custom languages on top of the bundled onessetLoader(name=>customs[name]??defaultLoader(name));// or allow only the languages your bundler can code-splitsetLoader(name=>({js: ()=>import('@speed-highlight/core/languages/js.js'),css: ()=>import('@speed-highlight/core/languages/css.js'),})[name]?.());

For full tree-shaking skip the loader entirely: tokenizeWith takes every language from the caller, so a bundler keeps only what you import. Include the sub-languages a grammar embeds (html uses css and js; js uses jsdoc, todo, and regex). A sub that is not given keeps the type of its rule and only skips the inner highlighting:

import{tokenizeWith}from'@speed-highlight/core/tokenize';import{html,css,js,jsdoc,todo,regex}from'@speed-highlight/core/languages';tokenizeWith(code,html,(str,type)=>{/* ... */},{languages: { css, js, jsdoc, todo, regex }});

Note

highlightHTML and tokenizeWith never touch the DOM, so they also run server-side or in a web worker: highlight there and send the string over.

CDN (no build step)

<linkrel="stylesheet" href="https://cdn.jsdelivr.net/npm/@speed-highlight/core@2/dist/themes/default.css"><scripttype="module">import{highlightAll}from'https://cdn.jsdelivr.net/npm/@speed-highlight/core@2/dist/index.js';highlightAll();</script>

Terminal usage

highlightANSI returns a string ready to print; the theme is required, import one from themes/*.js:

import{highlightANSI}from'@speed-highlight/core';importthemefrom'@speed-highlight/core/themes/atom-dark.js';console.log(awaithighlightANSI(code,'js',theme));

A terminal theme is a plain token-to-escape map, built with the termcolor helpers or raw escapes:

import*ascolfrom'@speed-highlight/core/themes/termcolor.js';exportdefault{kwd: col.red,str: col.green,cmnt: col.gray,};

For Deno, use the deno module:

import{highlightANSI}from'https://deno.land/x/speed_highlight_js/dist/index.js';importthemefrom'https://deno.land/x/speed_highlight_js/dist/themes/default.js';console.log(awaithighlightANSI('console.log("hello")','js',theme));

API

The main entry covers most apps; reach for /tokenize when you want the raw token stream and full control over what gets bundled. Everything ships TypeScript types.

EntryExportDescription
@speed-highlight/corehighlightAll(opt?)Highlight every element with a shj-lang-* class
highlightElement(elm, lang?, opt?)Highlight one element (language read from its class by default)
highlightHTML(src, lang, opt?)Highlight a string, resolves to an HTML string
highlightANSI(src, lang, theme)Highlight a string, resolves to an ANSI string for terminals
tokenize(src, lang, onToken)Loader-based tokenizer, calls onToken(text, type)
setLoader(loader) / defaultLoaderReplace or compose how language names are resolved
.../detectdetectLanguage(code)Guess the language, 'plain' when unsure
.../tokenizetokenizeWith(src, lang, onToken, opt?), tokenizerRegistry-free synchronous tokenizer (and the underlying generator), languages passed by the caller
.../languagesone named export per languageGrammars, import only what you need
.../themes/*.cssWeb themes
.../themes/*.jsTerminal themes, plus termcolor.js helpers

lang is a name ('js') or a grammar object passed directly. opt is { block?: boolean, showLineNumbers?: boolean }: line numbers are opt-in, block defaults to true, except that highlightElement and highlightAll read it off the element instead, where a code element is inline and anything else is a block.

Languages

NameCSS ClassSupportDetectionSize (gzip, 14.2 kB total)
Assemblyshj-lang-asm194 B
Bashshj-lang-bash430 B
Brainfuckshj-lang-bfincrement, operator, print, comment137 B
Cshj-lang-c429 B
CSSshj-lang-csscomment, str, selector, units, function, ...343 B
CSVshj-lang-csvpunctuation, ...96 B
Diffshj-lang-diff144 B
Dockerfileshj-lang-docker566 B
Gitshj-lang-gitcomment, insert, deleted, string, ...222 B
Goshj-lang-go329 B
HTMLshj-lang-html627 B
HTTPshj-lang-httpkeywork, string, punctuation, variable, version986 B
INIshj-lang-ini158 B
Javashj-lang-java457 B
JavaScriptshj-lang-jsbasic syntax, regex, jsdoc, json, template literals⛔ reported as TypeScript758 B
JSDocshj-lang-jsdoc247 B
JSONshj-lang-jsonstring, number, bool, ...172 B
LeanPub Markdownshj-lang-leanpub-md1.2 kB
Logshj-lang-lognumber, string, comment, errors223 B
Luashj-lang-lua273 B
Makefileshj-lang-make223 B
Markdownshj-lang-md1.1 kB
Perlshj-lang-pl329 B
Plain textshj-lang-plain71 B
Pythonshj-lang-py416 B
Regexshj-lang-regexcount, set, ...172 B
Rustshj-lang-rs414 B
SQLshj-lang-sqlnumber, string, function, ...1.7 kB
TODOshj-lang-todo185 B
TOMLshj-lang-tomlcomment, table, string, bool, variable236 B
TypeScriptshj-lang-tsjs syntax, ts keyword, types849 B
URIshj-lang-uri176 B
XMLshj-lang-xml511 B
YAMLshj-lang-yamlcomment, numbers, variable, string, bool208 B

Themes

NameTerminal (gzip)Web (gzip)
default174 B603 B
atom-dark174 B699 B
dark695 B
github-dark691 B
github-dim700 B
github-light672 B
visual-studio-dark694 B

Custom languages

A language is an array of rules. Every rule's regex (global flag required) is tried; the earliest match in the string wins, ties go to the earlier rule:

exportdefault[{match: /\/\/.*/g,type: 'cmnt'},{expand: 'str'},{expand: 'num'},{match: /\b(if|else|for|while|return)\b/g,type: 'kwd'},];
  • { match, type } tags what the regex matches with a token type
  • { expand } reuses a shared pattern: 'num', 'str', or 'strDouble'
  • { match, sub } re-tokenizes the matched region with another language: a name (loaded through the loader), an inline grammar array, or a function code => name | grammar deciding per match

A language can also set a default token for unmatched text by exporting { type, sub } instead of a bare array (see http.js). Use a grammar by passing it directly as lang, or register a name with setLoader. To extend an existing language, spread it after your rules:

importjsfrom'@speed-highlight/core/languages/js.js';exportdefault[{match: /\b(signal|effect)\b/g,type: 'func'},
...js,];

Token types:

TokenUsed forTokenUsed forTokenUsed for
kwdkeywordstypetypesescescape sequences
strstringsclassclassessectionsection delimiters
numnumbersvarvariablesinsertinserted parts (diff)
cmntcommentsoperoperatorsdeleteddeleted parts (diff)
funcfunctionsboolbooleanserrerrors

Missing a language? Open an issue or send a PR adding a file to src/languages/.

Custom themes

A web theme colors the token classes; start from default.css and override:

[class*="shj-lang-"] { color:#f8f8f2; background:#282a36; }
.shj-syn-kwd { color:#ff79c6; }
.shj-syn-str, .shj-syn-insert { color:#50fa7b; }
.shj-syn-cmnt { color:#6272a4; font-style: italic; }
.shj-numbers { color:#6272a4; }

Display-mode hooks: .shj-inline (inside code), .shj-block, and .shj-numbers for the gutter. Terminal themes are the token-to-escape maps shown in Terminal usage.

Migrating from v1

v1v2
highlightText(src, lang)highlightHTML(src, lang)
printHighlight(src, lang) from /terminalconsole.log(await highlightANSI(src, lang, theme))
setTheme('atom-dark')pass the theme: highlightANSI(src, lang, theme)
loadLanguage(name, grammar)setLoader(...) or pass the grammar directly as lang
@speed-highlight/core/terminal entrymerged into @speed-highlight/core
common.js shared patterns{ expand: 'num' | 'str' | 'strDouble' } built into the tokenizer
{ hideLineNumbers: true }now the default, line numbers are opt-in with { showLineNumbers: true }
oneline display moderemoved, a div is always a block
highlightElement(elm, lang, mode, opt)the mode moved into the options: highlightElement(elm, lang, { block })
shj-multiline classshj-block

Benchmark

$ npmrunbenchmarknodev26.7.0,darwinarm64,AppleM4corpus: js,css,json,md,sql,py,bash,tiledto3sizes(tiny(1KB) / medium(16KB) / huge(128KB)),medianof9trialsperlanguage,averagedacrossthecorpustiny(1KB)medium(16KB)huge(128KB)speed-highlight1,144,010ops/min74,720ops/min8,583ops/minprismjs655,152ops/min34,310ops/min2,575ops/minhighlight.js595,113ops/min48,666ops/min5,473ops/minsugar-high238,406ops/min13,405ops/min1,376ops/minshiki(jsengine)89,925ops/min6,682ops/min841ops/mincoldstart(import + firsthighlightoftest.js):
speed-highlight7.3msprismjs8.6mshighlight.js19mssugar-high18msshiki(jsengine)169msspeed-highlightperlanguage(warm,medianof9trials):
tiny(1KB)medium(16KB)huge(128KB)js666,642ops/min40,773ops/min4,704ops/mincss1,006,895ops/min62,774ops/min7,284ops/minjson1,964,840ops/min127,890ops/min15,057ops/minmd1,080,739ops/min86,883ops/min10,453ops/minsql1,369,247ops/min88,513ops/min10,180ops/minpy974,179ops/min56,165ops/min6,260ops/minbash945,526ops/min60,045ops/min6,141ops/min

Identical inputs from examples/languages/, tiled up to each size bucket, HTML-string output for every library, one op = one highlighted file. Each figure is the median of 9 repeated trials, reported as ops/min (the huge bucket can drop below 1 op/sec for the slower libraries). Warm runs have grammars preloaded; cold start is import plus first highlight, measured once (not size-swept). Shiki does more work by design (see Comparison).

About

A tiny, fast, simple syntax highlighter for the web and the terminal in JavaScript

Topics

Resources

Stars

401 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages