Syntax Highlighting with Prism.js and Next.js

Integrating syntax highlighting into a Next.js project has evolved with the latest Next.js versions (12–14) and React Server Components. This guide shows a modern, performant approach to syntax highlighting using Prism.js, including lazy highlighting, a copy-to-clipboard button, and MDX integration.

1. Install Dependencies

Install Prism.js and a tree-shakable icon library for copy buttons:

npm install prismjs
npm install https://github.com/react-icons/react-icons/releases/download/v5.4.0/react-icons-all-files-5.4.0.tgz

The @react-icons/all-files package allows importing only the icons you need, keeping bundle size small.

2. Import Prism Styles

In your app/layout.tsx (or _app.tsx if using the Pages Router), import your Prism CSS:

import "@/styles/prism.css";

Download a custom Prism CSS theme here: Prism Download. Save it under styles/prism.css.

3. Create a Syntax Highlighter Component

Highlight code only when visible using IntersectionObserver:

// SyntaxHighlighter.tsx
"use client";

import Prism from "prismjs";
import { useEffect, useRef } from "react";

// Import only needed Prism languages
import "prismjs/components/prism-bash";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-tsx";
import "prismjs/components/prism-python";
import "prismjs/components/prism-sql";
import "prismjs/components/prism-yaml";
import "prismjs/components/prism-nginx";
import "prismjs/components/prism-git";
import "prismjs/components/prism-json";
import "prismjs/components/prism-docker";
import "prismjs/components/prism-powershell";

interface SyntaxHighlighterProps {
language?: string;
code?: string;
}

const SyntaxHighlighter = ({ language, code }: SyntaxHighlighterProps) => {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
Prism.highlightAllUnder(entry.target);
}
});
},
{ rootMargin: "100%" }
);

if (ref.current) observer.observe(ref.current);
return () => {
if (ref.current) observer.unobserve(ref.current);
};
}, []);

return (


{code?.trim() ?? ""}


);
};

export default SyntaxHighlighter;

4. Add a Copy-to-Clipboard Button

Provide instant copy feedback with icons:

// CopyCodeButton.tsx
"use client";

import { FaCopy } from "@react-icons/all-files/fa/FaCopy";
import { FaCheck } from "@react-icons/all-files/fa/FaCheck";
import { useState } from "react";
import styles from "./CopyCodeButton.module.css";

export default function CopyCodeButton({ children }) {
const [copied, setCopied] = useState(false);

const handleClick = () => {
navigator.clipboard.writeText(children.props.children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (


{copied ? (
<>
<FaCheck className={`${styles.icon} ${styles.iconCopied}`} /> Copied!
</>
) : (
<>
<FaCopy className={`${styles.icon} ${styles.iconCopy}`} /> Copy code
</>
)}


);
}

5. Integrate with MDX / Markdown

Override the code component in your MDX renderer:

// renderers.tsx
import SyntaxHighlighter from "./SyntaxHighlighter";
import CopyCodeButton from "./CopyCodeButton";
import styles from "./Markdown.module.css";

export const markdownComponents = {
code: ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
if (!inline && match) {
return (

<SyntaxHighlighter language={match[1]} code={children} />
<CopyCodeButton>{children}</CopyCodeButton>

);
}
return (

{children}

);
},
};

6. Wrap MDX Content in a Client Component

Here’s how to pass your custom renderers to MDX using next-mdx-remote:

// mdxWrapper.tsx
"use client";

import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
import { markdownComponents as renderers } from "../renderers/renderers";
import styles from "./mdxWrapper.module.css";

const MDXWrapper: React.FC<{ content: MDXRemoteProps }> = ({ content }) => {
return (

<MDXRemote {...content} components={renderers} />

);
};

export default MDXWrapper;

This ensures:

Syntax highlighting is client-side only (avoiding SSR hydration issues).

Custom MDX renderers (e.g., SyntaxHighlighter, CopyCodeButton) are applied.

Styles and copy functionality work seamlessly for all code blocks.

7. Benefits

Lazy highlighting improves performance.

Only required Prism languages are loaded.

Users can copy code with a single click.

Fully compatible with Next.js App Router + MDX.

Can still switch to pre-rendered highlighting (rehype-prism-plus or rehype-pretty-code) for build-time performance if desired.

This approach is fully Next.js 14 / React 18 ready, client-friendly, and keeps bundle size minimal.