There was one feature that bothered me while building my portfolio site 1chooo.com: how to support GitHub-style code syntax highlighting in code blocks. I tried many solutions like sugar-high
, highlight.js
, and react-syntax-highlighter
, but I finally found a simpler and more elegant approach — using Shiki
.
In this post, I'll walk you through how I implemented syntax highlighting for MDX code blocks using Shiki, in combination with next-mdx-remote
1.
1. Install Dependencies
First, make sure you've installed all the necessary packages.
I use pnpm as my package manager. Make sure it's installed
before running the commands below. Or you can also use npm
or yarn
if you
prefer.
pnpm add @shikijs/rehype next-mdx-remote
2. Create a Custom MDX Component
Now let's declare a CustomMDX
component to render the MDX content with custom components and MDX options.
import { MDXRemote } from "next-mdx-remote/rsc";
function CustomMDX(props) {
return (
<MDXRemote
{...props}
components={{ ...components, ...props.components }}
options={{}} // Add MDX options later
/>
);
}
3. Build the CodeBlock Component
Next, we'll create a CodeBlock
component that supports both syntax highlighting and a copy-to-clipboard button.
"use client";
import React, { useRef, useState } from "react";
interface CodeBlockProps extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
}
export function CodeBlock({ className, children, ...props }: CodeBlockProps) {
const preRef = useRef<HTMLPreElement>(null);
const getCodeText = () => {
if (preRef.current) {
const codeElement = preRef.current.querySelector("code");
return codeElement?.textContent || "";
}
return "";
};
return (
<div className="relative group">
<pre ref={preRef} data-line-numbers className={className} {...props}>
{children}
</pre>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<CopyButton text={getCodeText()} />
</div>
</div>
);
}
interface CopyButtonProps {
text: string;
className?: string;
}
export function CopyButton({ text, className }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
const buttonClassName = [
"absolute top-2 right-2 px-2 py-1 text-xs font-medium",
"bg-gray-800 text-gray-200 rounded border border-gray-600",
"hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500",
"transition-all duration-200",
copied ? "bg-green-600 hover:bg-green-600" : "",
className || "",
]
.filter(Boolean)
.join(" ");
return (
<button
onClick={handleCopy}
className={buttonClassName}
aria-label="Copy code to clipboard"
>
{copied ? "Copied!" : "Copy"}
</button>
);
}
4. Configure Shiki with @shikijs/rehype
Now let's add Shiki support inside the CustomMDX
component using the rehype
plugin.
We'll use both github-light
and github-dark
themes as examples. You can browse more Shiki themes here.
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypeShiki from "@shikijs/rehype";
let components = {
code: (props: ComponentPropsWithoutRef<"code">) => (
<code className={styles.code} {...props} />
),
pre: ({ ...props }: React.HTMLAttributes<HTMLElement>) => (
<CodeBlock className={cn(styles.pre)} {...props} />
),
... // other components
};
function CustomMDX(props) {
return (
<MDXRemote
{...props}
components={{ ...components, ...props.components }}
options={{
mdxOptions: {
rehypePlugins: [
[
rehypeShiki,
{
themes: {
light: "github-light",
dark: "github-dark",
},
addLanguageClass: true,
defaultColor: false,
},
],
],
},
}}
/>
);
}
5. Set Up light/dark mode
To make the code blocks responsive to light/dark mode, add the following CSS (in your global stylesheet or a CSS Module):
@media (prefers-color-scheme: dark) {
pre code span {
color: var(--shiki-dark) !important;
}
pre {
background-color: var(--shiki-dark-bg) !important;
}
}
@media (prefers-color-scheme: light) {
pre code span {
color: var(--shiki-light) !important;
}
pre {
background-color: var(--shiki-light-bg) !important;
}
}
This ensures that the proper theme colors are applied automatically based on the user's system preference.
6. Try It Out
You can now test your MDX syntax highlighting by adding the following block:
```ts
console.log("hello MDX");
```
Here's how it renders:
<div class="relative group">
<pre
data-line-numbers
class="shiki shiki-themes github-light github-dark"
style="--shiki-light: #24292e; --shiki-dark: #e1e4e8; --shiki-light-bg: #fff; --shiki-dark-bg: #24292e;"
>
<code class="language-ts">
<span class="line">
<span>console.</span>
{...}
</span>
</code>
</pre>
</div>
If your code block contains the shiki
and shiki-themes github-light github-dark
classes, then your syntax highlighting setup is working correctly. You'll also see CSS variables like --shiki-light
, --shiki-dark
, etc., applied to your styles.
Conclusion
This setup gives you clean, GitHub-style syntax highlighting with minimal configuration — and it's fully compatible with MDX, dark mode, and custom components. If you're looking for a beautiful and maintainable way to render code blocks in your blog or portfolio, Shiki is a great choice.