Syntax Highlighting with Shiki, React Server Components, and Next.js

Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

February 8, 2024 · 3 min read

New Next.js logo

2025 Update

The article was updated to make a singleton highlighter, as mentioned in the latest Shiki update, to resolve certain memory leaks when the highlighter is called from React Server Components. Here’s a list of the updates:

  • Updated getHighlighter to be a singleton with makeSingletonHighlighter

  • Removed custom Blade language as now it’s bundled with Shiki!

Using Shiki for syntax highlighting

Wouldn’t it be great if we could provide syntax highlighting to our readers without adding any extra JavaScript weight? That’s exactly what we’re going to do using Shiki, React Server Components (RSC), and Next.js.

Installing Shiki

First, we need to install Shiki:

npm install -D shiki

We create a React Server Component. In Next.js’s App Router, Server Components are the default:

export default async function Code({ code, language }) {
	return <div></div>;
}

We get a code and a language from the props in our component. The code prop might come from a CMS and usually has the HTML code block for our code examples. The language prop shows which programming language the syntax highlighter should use.

Creating a Helper Function

To keep our code clean, we will wrap the syntax highlighting logic in a helper function. Go ahead and make a file called shiki.js inside the utils folder, and add the following content:

import { createHighlighter, makeSingletonHighlighter } from 'shiki';
import { bundledLanguages } from 'shiki/bundle/web';

import antlers from '../../content/languages/antlers.json';

const getHighlighter = makeSingletonHighlighter(createHighlighter);

export const codeToHtml = async ({ code, language }) => {

  const highlighter = await getHighlighter({
    themes: ['github-light', 'github-dark'],
    langs: [
      ...Object.keys(bundledLanguages),
      {
        id: 'antlers',
        scopeName: 'text.html.statamic',
        embeddedLangs: ['html'],
        ...antlers,
      },
    ],
  });

  return highlighter.codeToHtml(code, {
    lang: language,
    themes: {
      dark: 'github-dark',
      light: 'github-light',
    },
  });
};

Let’s break this down:

  1. We import the web bundle from Shiki to minimize the load. If you need support for additional languages, you can import the full bundle without worrying about client-side load since this occurs server-side.

  2. We use the getHighlighter function from Shiki to configure our highlighter with additional features before performing the syntax highlighting.

  3. We’ve set up two themes for light and dark modes. If your site doesn’t flip between modes, you can just stick with one theme.

  4. Shiki has built-in support for a lot of themes. Feel free to check the list.

const highlighter = await getHighlighter({
	theme: 'github-dark',
});

Custom languages with Shiki

We also include support for custom languages. For example, we’ve added Antlers for our Statamic ( Hey Statamic friends 👋 ) audience. We sourced Antlers from this repository.

Syntax highlighting

At the bottom of our JavaScript helper, we export the highlighter and the codeToHtml function, which accepts our language and themes and returns syntax-highlighted HTML.

return highlighter.codeToHtml(code, {
    lang: language,
    themes: {
      dark: 'github-dark',
      light: 'github-light',
    },
});

React Server component for syntax highlighting

With our helper function ready, let’s integrate it into our React Server Component:

import { codeToHtml } from '@/utils/shiki';

export default async function Code({ code, language }) {

  const html = await codeToHtml({
    code,
    language,
  });

  return <div className="px-5" dangerouslySetInnerHTML={{ __html: html }} />;
}

CSS for dual theme support

With our component and syntax highlighting set up, we can improve the user experience by adding CSS styles that match the user’s theme preferences. This step is optional but recommended if your site supports light and dark modes.

To implement theme-based rendering, you’ll need to have TailwindCSS configured with darkMode set to class.

Here’s an example of how you can define your CSS to switch between themes:

html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  /* Optional, if you also want font styles */
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}

/* Optional, for the code block we add overflow, borders and padding */
.shiki {
  @apply overflow-x-auto rounded-xl p-5;
}

By adding these styles to your CSS file and making sure your site’s HTML has the right classes, you can give users a smooth and nice-looking experience, no matter if they like light or dark mode.

Don’t forget to test your styles to make sure they change right based on what the user likes and that the code highlighting is easy to read and looks good in both themes.

Conclusion

We’ve set up beautiful, syntax-highlighted code blocks without adding any extra JavaScript for our readers. Have fun using Shiki, React Server Components, and Next.js in your projects.

Bring Your Ideas to Life 🚀

If you need help with a Laravel project let’s get in touch.

Lucky Media is proud to be recognized as a leading Next.js Development Agency

Technologies

Next.jsReact
Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

Founder and CEO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork

Related posts

February 16, 2023

Learn Laravel Routing Techniques for Next.js
Next.js
Laravel