Skip to content

Fix: Shiki Not Working — No Syntax Highlighting, Wrong Theme, or Build Errors

FixDevs ·

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 color

Or the build fails with a bundle error:

Error: Cannot find module 'shiki/langs/typescript.mjs'
// Or: Dynamic require of "fs" is not supported

Or a language isn’t recognized:

Error: Language 'prisma' is not loaded

Why 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 codeToHtml loads 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/web includes common languages; custom ones need registration.
  • Shiki is server-side only by default — the full Shiki bundle uses fs for loading grammars. In browser or edge environments, use shiki/bundle/web or 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 style attributes with color values 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/transformers
import { 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles