Skip to content

Fix: Prism React Renderer Not Working — No Syntax Colors, Wrong Language, or Custom Theme Not Applying

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix prism-react-renderer issues — Highlight component setup, language support, custom themes, line highlighting, copy button, and integration with MDX and documentation sites.

The Problem

The code block renders but has no colors:

import { Highlight, themes } from 'prism-react-renderer';

function CodeBlock({ code }: { code: string }) {
  return (
    <Highlight code={code} language="typescript" theme={themes.nightOwl}>
      {({ tokens, getLineProps, getTokenProps }) => (
        <pre>
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {line.map((token, j) => (
                <span key={j} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}
// Renders plain text without syntax highlighting

Or a language isn’t recognized:

Language "rust" is not supported

Or the v2 migration breaks existing code:

Module '"prism-react-renderer"' has no exported member 'Prism'

Why This Happens

prism-react-renderer is a React component that uses PrismJS for syntax highlighting without dangerouslySetInnerHTML. It wraps Prism’s tokenizer in a render-prop component, hands you token data, and asks you to render the spans yourself. That design is the source of most “no colors” bugs: if you forget to spread the token props, you get text but no inline style.

A second source of bugs is the bundling of Prism. In v1 you brought your own Prism instance and passed it as a prop, which meant you could share Prism plugins across packages. In v2 the package owns its Prism build, and any third-party Prism plugin (line highlight, custom language) has to be wired into the bundled instance via the exported Prism symbol. Code copied from a Gatsby or older Docusaurus tutorial often assumes the v1 ownership model and breaks silently on v2.

Common causes:

  • Version 2 changed the API significantly — v1 used Highlight as a default export with Prism passed as a prop. v2 exports Highlight as a named export and bundles Prism internally. Mixing v1 and v2 APIs causes import errors like Module ... has no exported member 'Prism' (when on v1) or defaultProps is not exported (when on v2).
  • Limited languages are bundled by default — to keep the bundle small, prism-react-renderer only includes roughly thirty common languages. Rust, Go, Ruby, and many others need additional Prism languages loaded separately.
  • The theme must be passed to Highlight — without a theme prop, tokens are generated but have no color styles. The render prop gives you un-styled tokens that you must apply styles to.
  • The style from getLineProps/getTokenProps contains the colors — if you override styles or do not spread the props, colors are lost.
  • SSR and hydration mismatch — themes that depend on the user’s color scheme can render differently on the server and the client. React 18 logs a hydration warning and may discard the server output entirely.

Version History That Changes the Failure Mode

prism-react-renderer rewrote its API once and is now slowly being replaced across the docs ecosystem by Shiki. Knowing which release you are on directly explains the import error you see.

  • prism-react-renderer v1.x (2018-2022) — required import Highlight, { defaultProps, Prism } from 'prism-react-renderer'. You spread {...defaultProps} on <Highlight> and passed a separately imported prismjs instance as the Prism prop. Code on v1 cannot be upgraded to v2 by changing imports alone — the prop API itself moved.
  • prism-react-renderer v2.0.0 (September 2023) — major rewrite. Highlight became a named export. Prism is bundled internally and exported as the named Prism symbol for extension. defaultProps was removed. Themes moved from a separate prism-react-renderer/themes/* import to the named themes export. Almost every “no colors after upgrade” or “Prism is not exported” report traces back to a half-finished v1→v2 migration.
  • prism-react-renderer v2.1 (late 2023) — added more bundled languages (TOML, Dart, Solidity-ish) and improved TypeScript types for PrismTheme. Custom themes written against v2.0 still work.
  • prism-react-renderer v2.3 (mid 2024) — line numbers exposed via a helper, plus better handling of trailing newlines that previously produced an extra empty token line at the bottom of every block.
  • prism-react-renderer v2.4 (late 2024) — current maintenance line. No breaking changes; mostly dependency bumps and React 19 compatibility tweaks for the render prop.
  • Ecosystem trend: Shiki migration (2023–2025) — Astro, Nextra, Docusaurus, and most documentation frameworks now default to Shiki (TextMate grammars) for build-time highlighting. Shiki produces real VS Code colors but requires WASM or async loading. If you are starting fresh, evaluate Shiki first; reach for prism-react-renderer when you need runtime highlighting of user-supplied code in the browser.

Fix 1: Basic Setup (v2)

npm install prism-react-renderer
'use client';

import { Highlight, themes } from 'prism-react-renderer';

interface CodeBlockProps {
  code: string;
  language: string;
}

function CodeBlock({ code, language }: CodeBlockProps) {
  return (
    <Highlight
      code={code.trim()}
      language={language}
      theme={themes.nightOwl}  // Built-in themes
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre
          className={className}
          style={{
            ...style,
            padding: '16px',
            borderRadius: '8px',
            overflow: 'auto',
            fontSize: '14px',
            lineHeight: '1.6',
          }}
        >
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {/* Optional: line numbers */}
              <span style={{
                display: 'inline-block',
                width: '2em',
                textAlign: 'right',
                marginRight: '1em',
                color: '#888',
                userSelect: 'none',
              }}>
                {i + 1}
              </span>
              {line.map((token, j) => (
                <span key={j} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}

// Available built-in themes:
// themes.dracula, themes.duotoneDark, themes.duotoneLight,
// themes.github, themes.jettwaveDark, themes.jettwaveLight,
// themes.nightOwl, themes.nightOwlLight, themes.oceanicNext,
// themes.okaidia, themes.oneDark, themes.oneLight,
// themes.palenight, themes.shadesOfPurple, themes.synthwave84,
// themes.ultramin, themes.vsDark, themes.vsLight

Fix 2: Add Unsupported Languages

import { Highlight, Prism, themes } from 'prism-react-renderer';

// prism-react-renderer bundles these languages:
// markup, jsx, tsx, swift, kotlin, objectivec, js-extras,
// reason, rust, graphql, yaml, go, cpp, markdown, python,
// css, javascript, typescript, json, bash, sql

// For additional languages, extend Prism:
// Note: In v2, Prism is exported from prism-react-renderer

// Add Ruby support
(typeof global !== 'undefined' ? global : window).Prism = Prism;
await import('prismjs/components/prism-ruby');

// Add PHP support
await import('prismjs/components/prism-php');

// Add Rust (already included) — just verify:
console.log(Prism.languages.rust);  // Should be defined

// Synchronous approach for build-time (SSR)
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-php';
import 'prismjs/components/prism-java';
import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-dart';
import 'prismjs/components/prism-toml';
import 'prismjs/components/prism-docker';

function RubyCodeBlock({ code }: { code: string }) {
  return (
    <Highlight code={code} language="ruby" theme={themes.oneDark}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre className={className} style={style}>
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {line.map((token, j) => (
                <span key={j} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}

Fix 3: Custom Theme

import { Highlight, type PrismTheme } from 'prism-react-renderer';

const myTheme: PrismTheme = {
  plain: {
    color: '#e0e0e0',
    backgroundColor: '#1a1a2e',
  },
  styles: [
    {
      types: ['comment', 'prolog', 'doctype', 'cdata'],
      style: { color: '#6a737d', fontStyle: 'italic' },
    },
    {
      types: ['string', 'attr-value'],
      style: { color: '#a5d6ff' },
    },
    {
      types: ['number', 'boolean'],
      style: { color: '#79c0ff' },
    },
    {
      types: ['keyword', 'operator'],
      style: { color: '#ff7b72' },
    },
    {
      types: ['function'],
      style: { color: '#d2a8ff' },
    },
    {
      types: ['class-name', 'tag'],
      style: { color: '#7ee787' },
    },
    {
      types: ['attr-name'],
      style: { color: '#79c0ff' },
    },
    {
      types: ['punctuation'],
      style: { color: '#8b949e' },
    },
    {
      types: ['variable', 'constant', 'symbol'],
      style: { color: '#ffa657' },
    },
    {
      types: ['builtin', 'char', 'selector'],
      style: { color: '#7ee787' },
    },
  ],
};

<Highlight code={code} language="typescript" theme={myTheme}>
  {/* ... */}
</Highlight>

Fix 4: Line Highlighting and Copy Button

'use client';

import { Highlight, themes } from 'prism-react-renderer';
import { useState } from 'react';

interface CodeBlockProps {
  code: string;
  language: string;
  filename?: string;
  highlightLines?: number[];  // Lines to highlight (1-indexed)
}

function CodeBlock({ code, language, filename, highlightLines = [] }: CodeBlockProps) {
  const [copied, setCopied] = useState(false);

  async function handleCopy() {
    await navigator.clipboard.writeText(code.trim());
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }

  return (
    <div style={{ position: 'relative', borderRadius: '8px', overflow: 'hidden' }}>
      {/* Header with filename and copy button */}
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: '8px 16px', background: '#1a1a2e', borderBottom: '1px solid #333',
        color: '#888', fontSize: '12px',
      }}>
        <span>{filename || language}</span>
        <button
          onClick={handleCopy}
          style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer' }}
        >
          {copied ? 'Copied!' : 'Copy'}
        </button>
      </div>

      <Highlight code={code.trim()} language={language} theme={themes.nightOwl}>
        {({ className, style, tokens, getLineProps, getTokenProps }) => (
          <pre className={className} style={{ ...style, margin: 0, padding: '16px', overflow: 'auto' }}>
            {tokens.map((line, i) => {
              const lineProps = getLineProps({ line });
              const isHighlighted = highlightLines.includes(i + 1);

              return (
                <div
                  key={i}
                  {...lineProps}
                  style={{
                    ...lineProps.style,
                    backgroundColor: isHighlighted ? 'rgba(255, 255, 255, 0.1)' : undefined,
                    borderLeft: isHighlighted ? '3px solid #60a5fa' : '3px solid transparent',
                    paddingLeft: '12px',
                    marginLeft: '-12px',
                    marginRight: '-12px',
                    paddingRight: '12px',
                  }}
                >
                  {line.map((token, j) => (
                    <span key={j} {...getTokenProps({ token })} />
                  ))}
                </div>
              );
            })}
          </pre>
        )}
      </Highlight>
    </div>
  );
}

// Usage
<CodeBlock
  code={`function greet(name: string) {
  console.log(\`Hello, \${name}!\`);
  return name;
}`}
  language="typescript"
  filename="greeting.ts"
  highlightLines={[2]}  // Highlight line 2
/>

Fix 5: MDX Integration

// components/MDXComponents.tsx — use as MDX code block renderer
import { Highlight, themes } from 'prism-react-renderer';

function MDXCodeBlock({ children, className }: {
  children: string;
  className?: string;
}) {
  // Extract language from className (e.g., "language-typescript")
  const language = className?.replace('language-', '') ?? 'text';

  // Inline code (no className) — render as <code>
  if (!className) {
    return (
      <code style={{
        background: '#f0f0f0',
        padding: '2px 6px',
        borderRadius: '4px',
        fontSize: '0.9em',
      }}>
        {children}
      </code>
    );
  }

  // Fenced code block — use Prism
  return (
    <Highlight code={children.trim()} language={language} theme={themes.nightOwl}>
      {({ className: hlClassName, style, tokens, getLineProps, getTokenProps }) => (
        <pre className={hlClassName} style={{ ...style, padding: '16px', borderRadius: '8px', overflow: 'auto' }}>
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {line.map((token, j) => (
                <span key={j} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}

// Register as MDX component
export const mdxComponents = {
  code: MDXCodeBlock,
  pre: ({ children }: any) => <>{children}</>,  // Unwrap <pre> — Highlight adds its own
};

Fix 6: Dual Theme (Light/Dark Mode)

'use client';

import { Highlight, themes } from 'prism-react-renderer';
import { useTheme } from 'next-themes';

function ThemedCodeBlock({ code, language }: { code: string; language: string }) {
  const { resolvedTheme } = useTheme();

  const theme = resolvedTheme === 'dark' ? themes.nightOwl : themes.nightOwlLight;

  return (
    <Highlight code={code.trim()} language={language} theme={theme}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre className={className} style={{ ...style, padding: '16px', borderRadius: '8px' }}>
          {tokens.map((line, i) => (
            <div key={i} {...getLineProps({ line })}>
              {line.map((token, j) => (
                <span key={j} {...getTokenProps({ token })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
}

Still Not Working?

Code renders as plain text (no colors) — verify you are spreading {...getTokenProps({ token })} on each <span>. The token props include inline style with the color from the theme. If you override style without spreading the original, colors are lost.

v1 code does not work after upgrading to v2 — v2 changed the API. import Highlight, { defaultProps, Prism } (v1) is now import { Highlight, themes, Prism } (v2). The {...defaultProps} spread on <Highlight> is no longer needed. Remove it and pass theme directly.

Language not recognized — prism-react-renderer includes roughly thirty languages. For others, install prismjs and import the language component: import 'prismjs/components/prism-ruby'. Set globalThis.Prism = Prism before importing so the new language attaches to the bundled instance, not a fresh one.

Hydration mismatch in Next.js — if the theme depends on client state (dark mode), the server render may not match the client. Use suppressHydrationWarning on the <pre> element, or render a placeholder during SSR and the actual code block after hydration.

Bundle size jumps after adding Prism languages — every import 'prismjs/components/prism-X' pulls in the language tokenizer. Audit your imports in production builds. For documentation sites where the language set is fixed, consider migrating to Shiki for build-time highlighting and zero client-side cost.

Line numbers off by one with trailing newline — pre-v2.3 builds add a phantom empty token line if the source string ends with \n. Call code.trimEnd() before passing to <Highlight>, or upgrade to v2.3+.

globalThis.Prism = Prism errors during SSRglobalThis is fine, but the Prism language module accesses document on import in some versions. Move language imports behind a dynamic import inside useEffect, or load them only in the client bundle by using next/dynamic with ssr: false.

For related code highlighting issues, see Fix: Shiki Not Working, Fix: MDX Not Working, Fix: Monaco Editor Not Working, and Fix: CodeMirror 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