TL;DR
Shiki uses VS Code's TextMate grammars — accurate, beautiful highlighting with VS Code themes, renders to HTML at build time, no client-side JavaScript needed, used by VitePress, Astro, and Nuxt Content. Prism is the lightweight client-side highlighter — extensible plugin system, token-based, 300+ languages, used by MDN and Gatsby. highlight.js is the classic auto-detecting highlighter — automatically detects language, 190+ languages, no dependencies, the most widely used. In 2026: Shiki for static/server-rendered docs, Prism for client-side with plugins, highlight.js for zero-config highlighting.
Key Takeaways
- Shiki: ~5M weekly downloads — VS Code grammars, server-side, zero client JS
- Prism: ~5M weekly downloads — lightweight, plugins (line numbers, copy), client-side
- highlight.js: ~10M weekly downloads — auto-detect language, zero-config, universal
- Shiki produces the most accurate highlighting (same engine as VS Code)
- Prism has the best plugin ecosystem (line highlighting, diff, toolbar)
- highlight.js is the easiest to set up (script tag + auto-detection)
Shiki
Shiki — VS Code-powered highlighting:
Basic usage
import { codeToHtml } from "shiki"
const html = await codeToHtml('console.log("Hello, World!")', {
lang: "javascript",
theme: "one-dark-pro",
})
console.log(html)
// → <pre class="shiki one-dark-pro" style="background-color:#282c34">
// → <code><span style="color:#E5C07B">console</span>...
// → </pre>
// Output is fully styled HTML — no client-side JS needed!
Multiple themes (light/dark)
import { codeToHtml } from "shiki"
// Dual theme — light + dark mode:
const html = await codeToHtml('const x = 42', {
lang: "typescript",
themes: {
light: "github-light",
dark: "github-dark",
},
})
// CSS to switch between themes:
// @media (prefers-color-scheme: dark) {
// .shiki { background-color: var(--shiki-dark-bg) !important; }
// .shiki span { color: var(--shiki-dark) !important; }
// }
Highlighter instance (performance)
import { createHighlighter } from "shiki"
// Create reusable highlighter (loads grammars once):
const highlighter = await createHighlighter({
themes: ["one-dark-pro", "github-light"],
langs: ["javascript", "typescript", "python", "rust", "json"],
})
// Fast repeated highlighting:
const html1 = highlighter.codeToHtml('const x = 1', { lang: "ts", theme: "one-dark-pro" })
const html2 = highlighter.codeToHtml('def main():', { lang: "python", theme: "one-dark-pro" })
const html3 = highlighter.codeToHtml('fn main() {}', { lang: "rust", theme: "one-dark-pro" })
// Dispose when done:
highlighter.dispose()
Transformers (line highlighting, diff, etc.)
import { codeToHtml } from "shiki"
import {
transformerNotationDiff,
transformerNotationHighlight,
transformerNotationFocus,
} from "@shikijs/transformers"
const code = `
const old = "removed" // [!code --]
const new = "added" // [!code ++]
const highlighted = 1 // [!code highlight]
const focused = true // [!code focus]
`
const html = await codeToHtml(code, {
lang: "typescript",
theme: "one-dark-pro",
transformers: [
transformerNotationDiff(), // Red/green diff lines
transformerNotationHighlight(), // Highlighted lines
transformerNotationFocus(), // Focus with dimmed context
],
})
Framework integrations
// VitePress — built-in Shiki:
// ```ts {1,3-4} ← line highlighting
// const a = 1 // [!code ++] ← diff
// ```
// Astro — built-in Shiki:
// astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
theme: "one-dark-pro",
wrap: true,
},
},
})
// Nuxt Content — built-in Shiki:
// nuxt.config.ts
export default defineNuxtConfig({
content: {
highlight: {
theme: "github-dark",
langs: ["js", "ts", "vue", "css", "html"],
},
},
})
Prism
Prism — lightweight client-side highlighter:
Basic setup
<!-- CDN (quickest setup): -->
<link href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-tomorrow.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-typescript.min.js"></script>
<!-- Your code blocks: -->
<pre><code class="language-typescript">
const greeting: string = "Hello, World!"
console.log(greeting)
</code></pre>
<!-- Prism auto-highlights on page load -->
Node.js / programmatic
import Prism from "prismjs"
import "prismjs/components/prism-typescript"
import "prismjs/components/prism-python"
import "prismjs/components/prism-rust"
const code = 'const x: number = 42'
const html = Prism.highlight(code, Prism.languages.typescript, "typescript")
console.log(html)
// → <span class="token keyword">const</span>
// → <span class="token literal-property">x</span>
// → <span class="token operator">:</span> ...
// Wrap in <pre><code>:
const block = `<pre class="language-typescript"><code>${html}</code></pre>`
Plugins
<!-- Line numbers: -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.css" rel="stylesheet" />
<pre class="line-numbers"><code class="language-js">...</code></pre>
<!-- Line highlight: -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-highlight/prism-line-highlight.min.js"></script>
<pre data-line="2,4-6"><code class="language-js">...</code></pre>
<!-- Copy to clipboard: -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
<!-- Diff highlight: -->
<pre><code class="language-diff-typescript diff-highlight">
- const old = "removed"
+ const new = "added"
</code></pre>
Themes
Built-in themes:
prism.css — Default (light)
prism-dark.css — Dark
prism-funky.css — Funky
prism-okaidia.css — Okaidia (Monokai)
prism-twilight.css — Twilight
prism-coy.css — Coy
prism-solarizedlight.css — Solarized Light
prism-tomorrow.css — Tomorrow Night
Community themes (prism-themes package):
prism-vsc-dark-plus — VS Code Dark+
prism-one-dark — Atom One Dark
prism-dracula — Dracula
prism-nord — Nord
prism-material-dark — Material Dark
highlight.js
highlight.js — auto-detecting highlighter:
Basic setup
<!-- CDN (simplest possible setup): -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github-dark.min.css" />
<script src="https://cdn.jsdelivr.net/npm/highlight.js/highlight.min.js"></script>
<script>hljs.highlightAll()</script>
<!-- Code blocks — language auto-detected! -->
<pre><code>
const greeting = "Hello, World!"
console.log(greeting)
</code></pre>
<!-- highlight.js detects this as JavaScript automatically -->
Node.js / programmatic
import hljs from "highlight.js"
// Auto-detect language:
const result = hljs.highlightAuto('const x = 42')
console.log(result.language) // → "javascript"
console.log(result.value) // → highlighted HTML
// Specify language:
const result2 = hljs.highlight('const x: number = 42', { language: "typescript" })
console.log(result2.value)
// → <span class="hljs-keyword">const</span>
// → <span class="hljs-attr">x</span>: ...
// Only load specific languages (smaller bundle):
import hljs from "highlight.js/lib/core"
import javascript from "highlight.js/lib/languages/javascript"
import typescript from "highlight.js/lib/languages/typescript"
hljs.registerLanguage("javascript", javascript)
hljs.registerLanguage("typescript", typescript)
Themes
Built-in themes (300+):
Popular dark themes:
github-dark.css
atom-one-dark.css
vs2015.css — VS Code dark
monokai.css
dracula.css
nord.css
tokyo-night-dark.css
Popular light themes:
github.css
atom-one-light.css
vs.css — VS Code light
stackoverflow-light.css
xcode.css
// Use theme:
<link rel="stylesheet" href="highlight.js/styles/github-dark.css" />
Language auto-detection
import hljs from "highlight.js"
// Auto-detect works by scoring each language:
const results = [
hljs.highlightAuto('print("hello")'), // → python
hljs.highlightAuto('console.log("hello")'), // → javascript
hljs.highlightAuto('fn main() { println!("hello"); }'), // → rust
hljs.highlightAuto('SELECT * FROM users'), // → sql
]
results.forEach((r) => {
console.log(`${r.language}: confidence ${r.relevance}`)
})
// Limit detection to specific languages:
const result = hljs.highlightAuto(code, ["javascript", "typescript", "python"])
With Markdown renderers
import MarkdownIt from "markdown-it"
import hljs from "highlight.js"
const md = new MarkdownIt({
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(str, { language: lang }).value
}
return hljs.highlightAuto(str).value
},
})
const html = md.render("```typescript\nconst x = 42\n```")
Feature Comparison
| Feature | Shiki | Prism | highlight.js |
|---|---|---|---|
| Engine | TextMate (VS Code) | Custom tokenizer | Custom parser |
| Rendering | Server-side (HTML) | Client-side (DOM) | Client/server |
| Client JS needed | ❌ | ✅ | ✅ |
| Language auto-detect | ❌ | ❌ | ✅ |
| Languages | 200+ | 300+ | 190+ |
| Themes | VS Code themes | CSS themes | 300+ CSS themes |
| Light/dark toggle | ✅ (CSS vars) | ✅ (swap CSS) | ✅ (swap CSS) |
| Line numbers | Via transformer | ✅ (plugin) | ✅ (plugin) |
| Line highlighting | ✅ (transformer) | ✅ (plugin) | ❌ |
| Diff highlighting | ✅ (transformer) | ✅ (plugin) | ✅ |
| Copy button | Via transformer | ✅ (plugin) | ❌ |
| Accuracy | Highest (VS Code) | Good | Good |
| Bundle size | 0 (server-side) | ~20KB + langs | ~30KB + langs |
| Used by | VitePress, Astro | MDN, Gatsby | Many CMS/blogs |
| Weekly downloads | ~5M | ~5M | ~10M |
When to Use Each
Use Shiki if:
- Building a static site or documentation (VitePress, Astro, Nuxt)
- Want the most accurate highlighting (VS Code quality)
- Need zero client-side JavaScript
- Want VS Code themes (One Dark Pro, GitHub Dark, etc.)
- Need line highlighting, diff, or focus transformers
Use Prism if:
- Need client-side highlighting with plugins
- Want line numbers, copy button, toolbar out of the box
- Building a CMS or user-facing code editor
- Need the largest language support (300+ languages)
- Prefer CSS-based theming
Use highlight.js if:
- Want the simplest setup (script tag + auto-highlight)
- Need automatic language detection
- Building a blog or content site with user-submitted code
- Want the largest theme collection
- Need Markdown renderer integration
Migration Guide
From Prism to Shiki (server-side rendering)
If you're on Prism with a static site generator and want to move to zero-client-JS highlighting:
// Before: Prism in a Node.js build step
import Prism from "prismjs"
import "prismjs/components/prism-typescript"
function highlight(code: string, lang: string): string {
return Prism.highlight(code, Prism.languages[lang], lang)
}
// After: Shiki server-side
import { createHighlighter } from "shiki"
const highlighter = await createHighlighter({
themes: ["one-dark-pro"],
langs: ["typescript", "javascript", "python"],
})
function highlight(code: string, lang: string): string {
return highlighter.codeToHtml(code, { lang, theme: "one-dark-pro" })
}
The main adjustment: Shiki returns a complete <pre><code> block with inline styles, while Prism returns the inner HTML that you wrap yourself. Update your rendering templates to avoid double-wrapping.
From highlight.js to Shiki (documentation sites)
For markdown-based documentation sites, switching from highlight.js auto-detection to Shiki requires specifying languages explicitly — the accuracy improvement is significant enough to justify it:
// Before: highlight.js with markdown-it
import MarkdownIt from "markdown-it"
import hljs from "highlight.js"
const md = new MarkdownIt({
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
return `<pre><code>${hljs.highlight(str, { language: lang }).value}</code></pre>`
}
return `<pre><code>${hljs.highlightAuto(str).value}</code></pre>`
},
})
// After: Shiki with markdown-it
import MarkdownIt from "markdown-it"
import { createHighlighter } from "shiki"
const highlighter = await createHighlighter({
themes: ["github-light", "github-dark"],
langs: ["javascript", "typescript", "python", "bash", "json"],
})
const md = new MarkdownIt({
highlight(str, lang) {
return highlighter.codeToHtml(str, {
lang: lang || "text",
themes: { light: "github-light", dark: "github-dark" },
})
},
})
Performance and Bundle Size Trade-offs
Bundle size and runtime performance vary significantly across these three libraries in ways that affect which is appropriate for different deployment targets. Shiki's grammar files and themes are loaded lazily — when you call createHighlighter({ langs: ['typescript', 'python'] }), only the specified language grammars are loaded into memory. For a documentation site that highlights 10 languages, the total grammar payload is around 500KB–1MB before compression. Because Shiki runs at build time and produces static HTML with inline styles, zero JavaScript is sent to the browser — the complete bundle size impact for end users is zero. This makes Shiki the clear choice for performance-critical static sites where Time to Interactive is a priority.
Prism's per-language files are individually loadable, allowing you to ship only the grammar definitions for the languages you actually use. The core prismjs bundle is roughly 20KB minified; each language component adds 3–30KB depending on complexity. For a browser-loaded site with TypeScript, JavaScript, Python, Bash, and JSON, the total Prism payload is around 70–80KB — small enough that it rarely appears in bundle analysis discussions. highlight.js with its full language suite is approximately 340KB uncompressed; the common optimization is to import only specific languages (highlight.js/lib/languages/javascript) reducing this to 50–100KB for typical web applications. highlight.js's auto-detection feature requires loading all candidate language grammars, which is why selective import is the recommended production pattern.
Accessibility and Semantic HTML Output
All three libraries produce accessible code block output, but with different approaches to semantic structure that affect screen reader behavior and browser default styles. Shiki generates <pre> and <code> elements with inline style attributes for colors — the semantic structure is correct, but inline styles bypass any CSS theming that downstream consumers might apply. Prism and highlight.js both use CSS class names on <span> elements, which makes overriding the theme via CSS straightforward. For design systems where the syntax highlighting theme must respect the application's design token system (e.g., using CSS custom properties for the color values), Shiki v1's dual-theme mode with CSS variable output (--shiki-light and --shiki-dark) bridges this gap by outputting custom properties instead of hardcoded hex values.
Screen readers handle code blocks consistently across all three libraries when the semantic markup is correct — a <pre><code> wrapper with a role="region" and aria-label="Code example" provides the necessary landmark. The language announcement (language-typescript class on the <code> element) is not announced by default by most screen readers, though some browser extensions for developers do surface it. For accessible documentation, pairing any of these highlighters with a visible language label above the code block is better UX than relying on the class name alone.
Community Adoption in 2026
Shiki reaches approximately 5 million weekly downloads, driven almost entirely by its inclusion in major documentation frameworks. VitePress (Vue's official documentation tool) uses Shiki as its default highlighter. Astro's built-in Markdown processing uses Shiki. Nuxt Content uses Shiki for code blocks. This means every VitePress site, Astro docs site, and Nuxt Content project is a Shiki user. The accuracy advantage — using the exact same TextMate grammar engine as VS Code — is a decisive factor for developer documentation sites where code quality perception matters. The @shikijs/transformers package adds VitePress-style annotations ([!code ++], [!code --], [!code highlight]) that have become the de facto standard for documenting code changes.
Prism maintains approximately 5 million weekly downloads, representing its legacy position across WordPress plugins, Gatsby sites, and content management systems. Prism's plugin system — offering line numbers, line highlighting, copy-to-clipboard, and diff highlighting — built up significant ecosystem momentum during the 2015-2020 era when it was the go-to choice. The MDN Web Docs use Prism for code examples. Gatsby's syntax highlighting plugins are predominantly Prism-based. In 2026, Prism is rarely the first choice for new projects, but it remains stable and widely deployed across existing sites. prismjs and react-syntax-highlighter (which uses Prism as its backend) together account for a significant share of the download count.
highlight.js reaches approximately 10 million weekly downloads, making it the most downloaded syntax highlighting library despite not being the most technically sophisticated. Its strength is simplicity: a script tag, a CSS file, and a single hljs.highlightAll() call. The automatic language detection — while not perfect — is genuinely useful for sites with mixed or user-submitted code where you cannot guarantee the language will be specified. Its 300+ theme collection (including atom-one-dark, github-dark, stackoverflow-dark) provides extensive customization without custom CSS. highlight.js is the default choice for lightweight CMS integrations, Hugo and Jekyll themes, and anywhere that a build step for Shiki would be overkill.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on Shiki v1.x, Prism v1.x, and highlight.js v11.x. Accessibility claims based on official ARIA documentation for each library. Bundle sizes from bundlephobia.com.
Compare developer tooling and documentation utilities on PkgPulse →
See also: AVA vs Jest and ohash vs object-hash vs hash-wasm, acorn vs @babel/parser vs espree.