Highlighting Code Snippets for MDX with React and Shiki

Cover Image for Highlighting Code Snippets for MDX with React and Shiki
Gagan Bodduluru
Gagan Bodduluru

Introduction

When building my blog with Next.js and MDX, I wanted the code snippets to actually look good and be easy to read. To get this working, I used rehype-pretty-code for the syntax highlighting (which uses Shiki under the hood), and I added a custom "Copy to Clipboard" button just to make things a bit more usable. Here is how I set everything up.


Integrating rehype-pretty-code with MDX

Setting Up rehype-pretty-code

First, I installed the packages I needed:

BASH
npm install rehype-pretty-code shiki

Next, I updated next.config.mjs to include the plugin in my MDX configuration. I set up the options to define both light and dark themes:

JS
import createMDX from '@next/mdx'
import rehypePrettyCode from "rehype-pretty-code";
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], // Include markdown and MDX files
}
 
/** @type {import('rehype-pretty-code').Options} */
const options = {
  keepBackground: false,
  theme: { 
    light: "everforest-light",
    dark: "everforest-dark"
  }
};
 
const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [[rehypePrettyCode, options]],
  }
})
 
export default withMDX(nextConfig)

This lets the syntax highlighting automatically switch between the everforest-light and everforest-dark themes depending on whether the user is in light or dark mode.

To make sure the background colors actually applied correctly, I added a few lines to my globals.css:

globals.css
CSS
/* Light theme styles */
code[data-theme*=" "], code[data-theme*=" "] span {
    color: var(--shiki-light) !important;
    background-color: var(--shiki-light-bg) !important;
}
 
/* Dark theme styles */
html.dark code[data-theme*=" "], html.dark code[data-theme*=" "] span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
}

Example Code Snippets

Here is what a standard code snippet looks like in my MDX file:

MD
```js
const greet = () => {
  console.log("Hello, World!");
};
greet();

And here is how it renders on the page:

JS
const greet = () => {
  console.log("Hello, World!");
};
greet();

If I need line numbers, I just add showLineNumbers to the markdown fence:

MD
```js showLineNumbers
const greet = () => {
  console.log("Hello, World!");
};
greet();

Which gives me this output:

JS
const greet = () => {
  console.log("Hello, World!");
};
greet();

Adding the "Copy to Clipboard" Feature

Overview of the Custom Component

Syntax highlighting is great, but having a "Copy to Clipboard" button makes life a lot easier when reading code tutorials.

I wrote a custom React component to handle this. It basically does three things:

  1. Takes the React code block elements and converts them into plain text, stripping out any HTML tags.
  2. Cleans up HTML entities so the copied text is formatted correctly.
  3. Swaps the copy icon for a checkmark for 3 seconds after you click it, so you know it worked.

Snippet from the React Component

Here is the specific logic I used for decoding and cleaning the text:

TSX
// Function to decode HTML entities
const decodeHtmlEntities = (text: string): string => {
  const textarea = document.createElement('textarea');
  textarea.innerHTML = text;
  return textarea.value;
};
 
// Function to strip HTML elements and extract plain text
const stripHtmlElements = (reactElement: ReactNode): string => {
  const staticMarkup = ReactDOMServer.renderToStaticMarkup(reactElement);
  const textContent = staticMarkup.replace(/<\/?[^>]+(>|$)/g, '');
  return decodeHtmlEntities(textContent.trim());
};

Integrating the Component with MDX

To get this to work with MDX, I overrode the default <pre> tag with my new component in mdx-components.tsx:

mdx-components.tsx
TSX
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
    pre: (props: JSX.IntrinsicElements["pre"] & { "data-language"?: string }) => (
      <CodeBlock reactElement={<pre {...props} />} language={props["data-language"] || "ts"} />
    ),
  };
}

Now every <pre> tag is automatically replaced with my CodeBlock component, which integrates perfectly with the HTML generated by rehype-pretty-code.


And that is basically it! The setup was pretty straightforward and makes reading code on the blog a much better experience.