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:
bashCopy codenpm 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:
tsCopy codeimport "@/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
:
tsxCopy code// 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 ( <div ref={ref}> <pre className={`language-${language}`} tabIndex={0}> <code className={`language-${language}`}>{code?.trim() ?? ""}</code> </pre> </div> ); }; export default SyntaxHighlighter;
4. Add a Copy-to-Clipboard Button
Provide instant copy feedback with icons:
tsxCopy code// 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 ( <div className={styles.copyButton} onClick={handleClick} title="Copy code"> <div className={styles.copyWrapper}> {copied ? ( <> <FaCheck className={`${styles.icon} ${styles.iconCopied}`} /> Copied! </> ) : ( <> <FaCopy className={`${styles.icon} ${styles.iconCopy}`} /> Copy code </> )} </div> </div> ); }
5. Integrate with MDX / Markdown
Override the code
component in your MDX renderer:
tsxCopy code// 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 ( <div className={styles.codeBlock}> <SyntaxHighlighter language={match[1]} code={children} /> <CopyCodeButton>{children}</CopyCodeButton> </div> ); } return ( <code className={styles.defaultCode} {...props}> {children} </code> ); }, };
6. Wrap MDX Content in a Client Component
Here’s how to pass your custom renderers to MDX using next-mdx-remote
:
tsxCopy code// 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 ( <div className={`${styles.contentWrapper} markdown`}> <MDXRemote {...content} components={renderers} /> </div> ); }; 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
orrehype-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.