Fix: Shiki Not Working — No Syntax Highlighting, Wrong Theme, or Build Errors
Quick Answer
How to fix Shiki syntax highlighter issues — basic setup, theme configuration, custom languages, transformer plugins, Next.js and Astro integration, and bundle size optimization.
The Problem
Shiki returns plain text without highlighting:
import { codeToHtml } from 'shiki';
const html = await codeToHtml('const x = 1;', { lang: 'typescript', theme: 'github-dark' });
// Returns HTML but no color tokens — everything is one colorOr the build fails with a bundle error:
Error: Cannot find module 'shiki/langs/typescript.mjs'
// Or: Dynamic require of "fs" is not supportedOr a language isn’t recognized:
Error: Language 'prisma' is not loadedWhy This Happens
Shiki is a syntax highlighter that uses TextMate grammars (the same as VS Code) for accurate highlighting. It works at build time or server-side:
- Shiki loads languages and themes asynchronously — grammars and theme files are JSON that must be fetched or imported. The first call to
codeToHtmlloads the requested language and theme. If loading fails silently, you get plain uncolored text. - Not all languages are bundled by default — Shiki supports 200+ languages, but to reduce bundle size, you may need to load them explicitly.
shiki/bundle/webincludes common languages; custom ones need registration. - Shiki is server-side only by default — the full Shiki bundle uses
fsfor loading grammars. In browser or edge environments, useshiki/bundle/webor pre-highlight at build time. - Themes determine the color output — if the theme isn’t loaded, tokens exist but have no color. The HTML output includes
styleattributes withcolorvalues from the theme.
Fix 1: Basic Usage
npm install shiki// Server-side highlighting
import { codeToHtml, codeToTokens } from 'shiki';
// Highlight code to HTML
const html = await codeToHtml(`
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
`, {
lang: 'tsx',
theme: 'github-dark',
});
// html = '<pre class="shiki github-dark" ...><code>...</code></pre>'
// Multiple themes (light/dark)
const htmlDual = await codeToHtml('const x: number = 42;', {
lang: 'typescript',
themes: {
light: 'github-light',
dark: 'github-dark',
},
// defaultColor: false, // Don't inline colors — use CSS variables
});
// Get tokens for custom rendering
const tokens = await codeToTokens('const x = 1;', {
lang: 'typescript',
theme: 'github-dark',
});
// tokens.tokens = [[{ content: 'const', color: '#ff7b72' }, ...]]Fix 2: Create a Highlighter Instance
import { createHighlighter } from 'shiki';
// Create a reusable highlighter — preload languages and themes
const highlighter = await createHighlighter({
themes: ['github-dark', 'github-light', 'one-dark-pro', 'vitesse-dark'],
langs: ['typescript', 'javascript', 'tsx', 'jsx', 'html', 'css', 'json', 'bash', 'python', 'rust', 'go'],
});
// Use the highlighter (synchronous after creation)
const html = highlighter.codeToHtml('const x = 1;', {
lang: 'typescript',
theme: 'github-dark',
});
// Load additional languages on demand
await highlighter.loadLanguage('prisma');
await highlighter.loadLanguage('graphql');
// Load additional themes
await highlighter.loadTheme('dracula');
// Check loaded languages/themes
console.log(highlighter.getLoadedLanguages());
console.log(highlighter.getLoadedThemes());
// Dispose when done (frees memory)
highlighter.dispose();Fix 3: Transformers (Line Numbers, Highlighting, Diffs)
npm install @shikijs/transformersimport { codeToHtml } from 'shiki';
import {
transformerNotationDiff,
transformerNotationHighlight,
transformerNotationFocus,
transformerMetaHighlight,
transformerNotationWordHighlight,
} from '@shikijs/transformers';
const html = await codeToHtml(`
const oldValue = 'hello'; // [!code --]
const newValue = 'world'; // [!code ++]
function important() { // [!code highlight]
return true;
}
console.log('focused'); // [!code focus]
`, {
lang: 'typescript',
theme: 'github-dark',
transformers: [
transformerNotationDiff(), // [!code --] and [!code ++]
transformerNotationHighlight(), // [!code highlight]
transformerNotationFocus(), // [!code focus]
transformerMetaHighlight(), // ```ts {1,3-5} — highlight lines
transformerNotationWordHighlight(), // [!code word:xxx]
],
});
// Add CSS for diff/highlight styles
/*
.shiki .diff.add { background-color: rgba(16, 185, 129, 0.15); }
.shiki .diff.remove { background-color: rgba(244, 63, 94, 0.15); }
.shiki .highlighted { background-color: rgba(101, 117, 133, 0.16); }
.shiki .has-focus .line:not(.focused) { opacity: 0.5; transition: opacity 0.3s; }
.shiki:hover .has-focus .line:not(.focused) { opacity: 1; }
*/
// Custom transformer — add line numbers
import type { ShikiTransformer } from 'shiki';
const lineNumbers: ShikiTransformer = {
name: 'line-numbers',
code(node) {
this.addClassToHast(node, 'line-numbers');
},
line(node, line) {
node.properties['data-line'] = line;
},
};Fix 4: Next.js Integration
// lib/shiki.ts — singleton highlighter
import { createHighlighter, type Highlighter } from 'shiki';
let highlighter: Highlighter | null = null;
export async function getHighlighter() {
if (!highlighter) {
highlighter = await createHighlighter({
themes: ['github-dark', 'github-light'],
langs: ['typescript', 'javascript', 'tsx', 'jsx', 'bash', 'json', 'css', 'html'],
});
}
return highlighter;
}// components/CodeBlock.tsx — Server Component
import { getHighlighter } from '@/lib/shiki';
interface CodeBlockProps {
code: string;
lang: string;
filename?: string;
}
export async function CodeBlock({ code, lang, filename }: CodeBlockProps) {
const highlighter = await getHighlighter();
const html = highlighter.codeToHtml(code.trim(), {
lang,
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: false,
});
return (
<div className="relative rounded-lg overflow-hidden my-4">
{filename && (
<div className="bg-gray-800 text-gray-400 text-xs px-4 py-2 border-b border-gray-700">
{filename}
</div>
)}
<div
className="[&_pre]:p-4 [&_pre]:overflow-x-auto [&_code]:text-sm"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
// Usage in Server Component
<CodeBlock
code={`const greeting = "Hello, World!";
console.log(greeting);`}
lang="typescript"
filename="example.ts"
/>/* Dual theme CSS — light/dark mode */
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}Fix 5: Astro Integration
// astro.config.mjs — Shiki is built into Astro
import { defineConfig } from 'astro/config';
export default defineConfig({
markdown: {
shikiConfig: {
theme: 'github-dark',
// Or dual themes:
// themes: { light: 'github-light', dark: 'github-dark' },
langs: [], // Add custom languages
wrap: true, // Word wrap
transformers: [],
},
},
});Fix 6: Bundle for Client-Side / Edge
// Use the web bundle for browser/edge environments
import { createHighlighterCore } from 'shiki/core';
import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
// Or use the lighter JavaScript regex engine
import { createJavaScriptRegExpEngine } from 'shiki/engine/javascript';
const highlighter = await createHighlighterCore({
themes: [
import('shiki/themes/github-dark.mjs'),
],
langs: [
import('shiki/langs/typescript.mjs'),
import('shiki/langs/javascript.mjs'),
],
engine: createJavaScriptRegExpEngine(), // No WASM — smaller bundle
});
// Pre-built web bundle with common languages
import { codeToHtml } from 'shiki/bundle/web';
const html = await codeToHtml('const x = 1;', {
lang: 'typescript',
theme: 'github-dark',
});Still Not Working?
Output has no colors — the theme isn’t loaded. When using createHighlighter, pass themes in the constructor. When using codeToHtml, the theme must be specified. Check the output HTML — if style attributes have color values, the theme loaded correctly.
“Language not loaded” error — the language must be preloaded in createHighlighter({ langs: [...] }) or loaded dynamically with highlighter.loadLanguage(). For codeToHtml (standalone function), languages are loaded automatically but may fail if the grammar file isn’t accessible.
Build fails with “Dynamic require of fs” — you’re importing the full Shiki bundle in a client-side or edge environment. Use shiki/bundle/web for browsers or createHighlighterCore with the JavaScript engine for edge runtimes.
Highlighting is slow — creating a highlighter is expensive (loads WASM + grammars). Create it once and reuse across requests. In Next.js, use a module-level singleton. Don’t call createHighlighter inside a component render.
For related code display issues, see Fix: MDX Not Working and Fix: Docusaurus Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.
Fix: Sharp Not Working — Installation Failing, Image Not Processing, or Build Errors on Deploy
How to fix Sharp image processing issues — native binary installation, resize and convert operations, Next.js image optimization, Docker setup, serverless deployment, and common platform errors.
Fix: Rspack Not Working — Build Failing, Loaders Not Applying, or Dev Server Not Starting
How to fix Rspack issues — configuration migration from webpack, loader compatibility, CSS extraction, module federation, React Fast Refresh, and build performance tuning.
Fix: Biome Not Working — Rules Not Applied, ESLint Config Not Migrated, or VSCode Extension Ignored
How to fix Biome linter/formatter issues — biome.json configuration, migrating from ESLint and Prettier, VSCode extension setup, CI integration, and rule override syntax.