rehype plugin to highlight code with starry-night
- What’s this?
- When should I use this?
- Install
- Use
- API
- Theming
- Examples
- Example: using aliases
- Example: using all
starry-nightgrammars - Example: using custom
starry-nightgrammar - Example: skip highlighting a specific language
- Example: show codeblock title
- Example: show codeblock language
- Example: show a prompt before a line
- Example: show highlighted, inserted and deleted lines
- Example: show line numbers
- Example: customize with your own plugin
- Example: customize classname prefix
- Related
- License
This package is a unified (rehype) plugin to highlight code with starry-night in a markdown document. It mimics GitHub's syntax highlighting.
This project is useful if you want to use the syntax highlighting powered by VS Code's syntax highlighter engine, and themes similar to GitHub. It is also useful if you want to build your own syntax highlighting themes based on CSS custom properties. You can enable additional features through plugins using Plugin API based on your specific need; they are all opt-in.
This package is ESM only.
In Node.js (version 16.0+), install with npm:
npm install @microflash/rehype-starry-nightIn Deno, with esm.sh:
import rehypeStarryNight from "https://esm.sh/@microflash/rehype-starry-night";In browsers, with esm.sh:
<script type="module">
import rehypeStarryNight from "https://esm.sh/@microflash/rehype-starry-night?bundle";
</script>Say you have the following file index.md:
```css
html {
box-sizing: border-box;
text-size-adjust: 100%;
/* allow percentage based heights for the children */
height: 100%;
}
```
And our module index.js looks as follows:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight)
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running that with node index.js yields:
<div class="hl">
<pre><code id="MC40OTU2MDAx" tabindex="0" class="language-css"><span class="pl-ent">html</span> {
<span class="pl-c1">box-sizing</span>: <span class="pl-c1">border-box</span>;
<span class="pl-c1">text-size-adjust</span>: <span class="pl-c1">100</span><span class="pl-k">%</span>;
<span class="pl-c">/* allow percentage based heights for the children */</span>
<span class="pl-c1">height</span>: <span class="pl-c1">100</span><span class="pl-k">%</span>;
}
</code></pre>
</div>The default export is rehypeStarryNight. The following options are available. All of them are optional.
namespace(type:string, default:hl): class name of the highlighted codeblockgrammars(type:Array<Grammar>, default:common) -starry-nightcompatible grammar definitions. By default, common grammars provided bystarry-nightare used.aliases(type:Record<string, string>, default:{}): aliases to force syntax highlighting. By default, unknown languages are not highlighted.plainText(type:Array<string>, default:[]): array of languages not to highlightplugins(type:Array<Plugin>, default:[]) - array of plugins to customize the highlighted codeblock, using Plugin APIallowMissingScopes(type:boolean, default:false) - whether to warn for missing scope or not
You can customize the highlighted codeblock with a plugin. A plugin looks like this:
export const myPlugin = {
type: "header",
opts: meta => {
// parse codeblock metadata
return {
// plugin specific options
}
},
apply: (opts, node) => {
// do something with plugin specific options and codeblock node
}
};type(type:string, possible values:header,lineorfooter) - controls whether the customization applies to the header, line or footer of the codeblock elementopts(type:function, arguments:meta, return:Object) - optional option processing function. You can use this to read the metadata associated with a codeblock (parsed byfenceparser) and return plugin specific options derived from them. By default, the metadata contains the language associated with the codeblock.apply(type:function, arguments:optsandnodes) - required function that applies customizations on the codeblock nodes (an array of hast nodes) based on plugin options (supplied byoptsfunction)
rehype-starry-night ships a few plugins out of box, with which you can
- add title to your codeblock
- show language of the codeblock
- show highlighted, inserted, and deleted lines
- show a prompt before a line to indicate a command line prompt
- mark lines as command output (so they are not copied alongwith command)
These plugins are opt-in and you'll have to manually import them.
import {
titlePlugin,
languageIndicatorPlugin,
lineAnnotationPlugin
} from "@microflash/rehype-starry-night/plugins";
const processor = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plugins: [
titlePlugin,
languageIndicatorPlugin,
lineAnnotationPlugin
]
})
.use(rehypeStringify, { allowDangerousHtml: true });Plugins are applied in the order they are added in options. In the above case, titlePlugin will be applied first followed by languageIndicatorPlugin and lineAnnotationPlugin.
Import props.css and index.css files in your project, or use them as a base for your own custom theme. For different color schemes for syntax highlighting, check the available themes on starry-night repository.
Here's one way to support light and dark themes; the appropriate theme will get activated based on system preferences.
:root {
/* light theme variables specific to rehype-starry-night plugin */
--hl-bg-color: hsl(220, 23%, 97%);
--hl-border-color: hsl(215, 15%, 85%);
--hl-outline-color: hsl(215, 15%, 70%, 0.5);
}
@media (prefers-color-scheme: dark) {
:root {
/* dark theme variables specific to rehype-starry-night plugin */
--hl-bg-color: hsl(216, 18%, 11%);
--hl-border-color: hsl(215, 11%, 22%);
--hl-outline-color: hsl(215, 11%, 37%, 0.5);
}
}
/* import a `starry-night` theme that supports both dark and light themes */
@import "https://raw.githubusercontent.com/wooorm/starry-night/main/style/both.css";
/* import CSS specific to rehype-starry-night plugin */
@import "https://raw.githubusercontent.com/Microflash/rehype-starry-night/main/src/index.css";Warning
URL imports for external styles is not recommended. You should either self-host them or bundle them.
Say you have the following file index.md:
```xjm
language: "en"
customization: false
features: [ "io", "graphics", "compute" ]
```
You can alias xjm to yml as follows with index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
aliases: {
xjm: "yml"
}
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC40OTU1Mzcz" tabindex="0" class="language-xjm"><span class="pl-ent">language</span>: <span class="pl-s"><span class="pl-pds">"</span>en<span class="pl-pds">"</span></span>
<span class="pl-ent">customization</span>: <span class="pl-c1">false</span>
<span class="pl-ent">features</span>: <span class="pl-s">[ "io", "graphics", "compute" ]</span>
</code></pre>
</div>By default, this plugin uses the common grammars provided by starry-night. You can import all grammars to highlight the code in languages not included in the common grammars.
Say, you have the following codeblock in index.md:
```ballerina
import ballerina/io;
public function main() {
io:println("Hello, World!");
}
```
Since, ballerina is not included in the common grammars, you'll have to import all grammars from the starry-night as follows in index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { all } from "@woorm/starry-night";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
grammars: all
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC42MjYzNjU1" tabindex="0" class="language-ballerina"><span class="pl-k">import</span> <span class="pl-smi">ballerina</span>/<span class="pl-smi">io</span>;
<span class="pl-k">public</span> <span class="pl-k">function</span> <span class="pl-en">main</span>() {
<span class="pl-smi">io</span><span class="pl-k">:</span><span class="pl-en">println</span>(<span class="pl-s"><span class="pl-pds">"</span>Hello, World!<span class="pl-pds">"</span></span>);
}
</code></pre>
</div>If you want to highlight a language for which starry-night does not provide a grammar, you can convert a Text Mate grammar to starry-night compatible format and use it alongside other grammar definitions.
Say, you have a custom grammar to highlight log files in text.log.js and you want to highlight the following file index.md:
```log
2025-05-18T23:08:48.269 INFO 10683 --- [main] com.zaxxer.hikari.HikariDataSource : H2HikariPool - Starting...
2025-05-18T23:08:48.338 INFO 10683 --- [main] com.zaxxer.hikari.pool.HikariPool : H2HikariPool - Added connection conn0: url=jdbc:h2:mem:sa user=SA
2025-05-18T23:08:48.338 INFO 10683 --- [main] com.zaxxer.hikari.HikariDataSource : H2HikariPool - Start completed.
```
You can import text.log.js alongside common grammars as follows in index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { common } from "@wooorm/starry-night";
import logGrammar from "./text.log.js";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
grammars: [
logGrammar,
...common
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC41ODc0NjE2" tabindex="0" class="language-log"><span class="pl-c">2025-05-18T23:08:48.269</span> <span class="pl-mi1">INFO</span> <span class="pl-c1">10683</span> --- [main] com.zaxxer.hikari.HikariDataSource : H2HikariPool - Starting...
<span class="pl-c">2025-05-18T23:08:48.338</span> <span class="pl-mi1">INFO</span> <span class="pl-c1">10683</span> --- [main] com.zaxxer.hikari.pool.HikariPool : H2HikariPool - Added connection conn0: url=jdbc:h2:mem:sa user=SA
<span class="pl-c">2025-05-18T23:08:48.338</span> <span class="pl-mi1">INFO</span> <span class="pl-c1">10683</span> --- [main] com.zaxxer.hikari.HikariDataSource : H2HikariPool - Start completed.
</code></pre>
</div>In some cases, you may want to process codeblocks of a specific language differently. Say you have the following file index.md where you want to render the Mermaid diagram instead of syntax highlighting:
```mermaid
flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
```
You can skip highlighting mermaid codeblock by listing it in plainText option as follows in the index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plainText: [
"mermaid"
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<pre><code class="language-mermaid">flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
</code></pre>Note
Codeblocks with unknown languages (that is, languages for which there are no available starry-night grammars) or no language are always skipped by the plugin.
You can add a title to a codeblock, like in the following file index.md:
```zsh title="Switching off homebrew telemetry"
# turns off homebrew telemetry
export HOMEBREW_NO_ANALYTICS=1
# turns off homebrew auto-update
export HOMEBREW_NO_AUTO_UPDATE=1
```
To show this title, import title plugin in index.js as follows:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { titlePlugin } from "@microflash/rehype-starry-night/plugins";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plugins: [
titlePlugin
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<div class="hl-header">
<div class="hl-title">Switching off homebrew telemetry</div>
</div>
<pre><code id="MC41NTAxMDgw" tabindex="0" class="language-zsh"><span class="pl-c"># turns off homebrew telemetry</span>
<span class="pl-k">export</span> HOMEBREW_NO_ANALYTICS=1
<span class="pl-c"># turns off homebrew auto-update</span>
<span class="pl-k">export</span> HOMEBREW_NO_AUTO_UPDATE=1
</code></pre>
</div>Say, you have the following file index.md:
```rust
fn main() {
println!("Hello, world!");
}
```
To display the language associated with this codeblock, import languageIndicator plugin in the index.js as follows:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { languageIndicatorPlugin } from "@microflash/rehype-starry-night/plugins";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plugins: [
languageIndicatorPlugin
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC4yMTUwNTAx" tabindex="0" class="language-rust"><span class="pl-k">fn</span> <span class="pl-en">main</span>() {
<span class="pl-en">println!</span>(<span class="pl-s"><span class="pl-pds">"</span>Hello, world!<span class="pl-pds">"</span></span>);
}
</code></pre>
<div class="hl-footer">
<div class="hl-language">rust</div>
</div>
</div>Sometime you may want to display a command and its output. You can specify lines that are command-line instructions and lines that are output, as follows in index.md:
```sh prompt{1} output{2..4}
eza --version
eza - A modern ls replacement
v0.23.4 [+git]
```
Import lineAnnotation plugin in the index.js as follows:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { lineAnnotationPlugin } from "@microflash/rehype-starry-night/plugins";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plugins: [
lineAnnotationPlugin
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC4wMDU4OTIz" tabindex="0" class="language-sh" style="--hl-line-gutter: 1"><span data-line-number="1"><span class="prompt" aria-hidden="true"></span>eza --version</span>
<span data-line-number="2" data-line-output="">eza - A modern ls replacement</span>
<span data-line-number="3" data-line-output="">v0.23.4 [+git]</span>
</code></pre>
</div>Note
The selection of prompt character should be disabled so that when people copy the command, the prompt is not copied. This behavior is implemented in index.css with user-select: none. Similarly, the output is also disabled for user selection because you usually want people to just copy the command and not the output.
You can highlight lines by specifying the line numbers (or even, range of line numbers) between curly braces in the codeblock metadata.
```sh {3,5..12} prompt{1}
curl http://localhost:12434/engines/llama.cpp/v1/chat/completions \
--json '{
"model": "ai/smollm2",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Write 500 words about the fall of Rome."
}
]
}'
```
Import lineAnnotation plugin (described in the previous example) and run node index.js which yields:
<div class="hl">
<pre><code id="MC43MzU2Njkz" tabindex="0" class="language-sh" style="--hl-line-gutter: 2"><span data-line-number="1"><span class="prompt" aria-hidden="true"></span>curl http://localhost:12434/engines/llama.cpp/v1/chat/completions \</span>
<span data-line-number="2"> --json <span class="pl-s"><span class="pl-pds">'</span>{</span></span>
<span data-line-number="3" data-line-marked=""><span class="pl-s"> "model": "ai/smollm2",</span></span>
<span data-line-number="4"><span class="pl-s"> "messages": [</span></span>
<span data-line-number="5" data-line-marked=""><span class="pl-s"> {</span></span>
<span data-line-number="6" data-line-marked=""><span class="pl-s"> "role": "system",</span></span>
<span data-line-number="7" data-line-marked=""><span class="pl-s"> "content": "You are a helpful assistant."</span></span>
<span data-line-number="8" data-line-marked=""><span class="pl-s"> },</span></span>
<span data-line-number="9" data-line-marked=""><span class="pl-s"> {</span></span>
<span data-line-number="10" data-line-marked=""><span class="pl-s"> "role": "user",</span></span>
<span data-line-number="11" data-line-marked=""><span class="pl-s"> "content": "Write 500 words about the fall of Rome."</span></span>
<span data-line-number="12" data-line-marked=""><span class="pl-s"> }</span></span>
<span data-line-number="13"><span class="pl-s"> ]</span></span>
<span data-line-number="14"><span class="pl-s"> }<span class="pl-pds">'</span></span></span>
</code></pre>
</div>Similarly, you can render inserted and deleted lines using ins and del properties on the codeblock followed by a range of line numbers.
```js title="Pool options in Vitest 2.0" del{4..6} ins{7..9}
export default defineConfig({
test: {
poolOptions: {
threads: {
singleThread: true,
},
forks: {
singleFork: true,
},
}
}
});
```
The above codeblock gets rendered as:
<div class="hl">
<pre><code id="MC4wNDgyMzEx" tabindex="0" class="language-js" style="--hl-line-gutter: 2"><span data-line-number="1"><span class="diff"> </span><span class="pl-k">export</span> <span class="pl-c1">default</span> <span class="pl-smi">defineConfig</span>({</span>
<span data-line-number="2"><span class="diff"> </span> test<span class="pl-k">:</span> {</span>
<span data-line-number="3"><span class="diff"> </span> poolOptions<span class="pl-k">:</span> {</span>
<span data-line-number="4" data-line-deleted=""><span class="diff">- </span> threads<span class="pl-k">:</span> {</span>
<span data-line-number="5" data-line-deleted=""><span class="diff">- </span> singleThread<span class="pl-k">:</span> <span class="pl-c1">true</span>,</span>
<span data-line-number="6" data-line-deleted=""><span class="diff">- </span> },</span>
<span data-line-number="7" data-line-inserted=""><span class="diff">+ </span> forks<span class="pl-k">:</span> {</span>
<span data-line-number="8" data-line-inserted=""><span class="diff">+ </span> singleFork<span class="pl-k">:</span> <span class="pl-c1">true</span>,</span>
<span data-line-number="9" data-line-inserted=""><span class="diff">+ </span> },</span>
<span data-line-number="10"><span class="diff"> </span> }</span>
<span data-line-number="11"><span class="diff"> </span> }</span>
<span data-line-number="12"><span class="diff"> </span>});</span>
</code></pre>
</div>Note
See the documentation of fenceparser to learn about the ways in which you can specify the line range.
When you import the lineAnnotation plugin (as seen in previous example), it adds data-line-number attribute to every line. It also attaches --hl-line-gutter CSS custom property on the code element. Using these two details, you can use the following CSS to show line numbers.
rehype-starry-night/src/index.css
Lines 76 to 83 in 48eaba1
For single line codeblocks, the numbers won't show up.
Suppose you want to add a copy to clipboard button in the footer. You can do so by adding a custom footer plugin.
Say you have the following file index.md:
```html
<mark>highlighted</mark>
```
You can pass a custom footer plugin as follows with index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { languageIndicatorPlugin } from "@microflash/rehype-starry-night/plugins";
import { h } from "hastscript";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
plugins: [
languageIndicatorPlugin,
{
type: "footer",
apply: (opts, node) => {
if (opts.id) node.push(h(`button.${opts.namespace}-copy`, "Copy to clipboard"));
}
}
]
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="hl">
<pre><code id="MC44MjIxMjIz" tabindex="0" class="language-html"><<span class="pl-ent">mark</span>>highlighted</<span class="pl-ent">mark</span>>
</code></pre>
<div class="hl-footer">
<div class="hl-language">html</div>
<button class="hl-copy">Copy to clipboard</button>
</div>
</div>Say you have the following file index.md:
```java
IO.println("Hello, world!");
```
You can customize the classname prefix of codeblock element by setting the namespace option, as follows with index.js:
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeStarryNight from "@microflash/rehype-starry-night";
import { readFileSync } from "node:fs";
main();
async function main() {
const markdown = readFileSync("./index.md", "utf8");
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStarryNight, {
namespace: "highlight"
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
console.log(String(file));
}Running this with node index.js yields:
<div class="highlight">
<pre><code id="MC45MjYyOTEy" tabindex="0" class="language-java"><span class="pl-c1">IO</span><span class="pl-k">.</span>println(<span class="pl-s"><span class="pl-pds">"</span>Hello, world!<span class="pl-pds">"</span></span>);
</code></pre>
</div>rehype-starry-night— alternative plugin to apply syntax highlighting to code withstarry-nightrehype-highlight— highlight code with highlight.js (through lowlight)rehype-prism-plus— highlight code with Prism (via refractor) with additional line highlighting and line numbers functionalities@shikijs/rehype— highlight code with shiki












